Bash'de “geri arama” programlama kavramı var mı?


21

Programlamayı okuduğumda birkaç kez "geri arama" kavramıyla karşılaştım.

Funnily, "geri arama fonksiyonu" terimi için "didaktik" veya "açık" diyebileceğim bir açıklama bulamadım (okuduğum hemen hemen her açıklama bana bir diğerinden yeterince farklı görünüyordu ve şaşkın hissettim).

Bash'de "geri arama" programlama kavramı var mı? Öyleyse, lütfen küçük, basit, Bash bir örnekle cevap verin.


2
"Geri arama" gerçek bir kavram mı yoksa "birinci sınıf fonksiyon" mu?
Cedric H.

declarative.bashBelirli bir değere ihtiyaç duyulduğunda çağrılacak işlevleri yapılandırılmış olarak açıkça kullanan bir çerçeve olarak ilginç bulabilirsiniz .
Charles Duffy

İlgili başka bir çerçeve: bashup / olaylar . Belgeleri, doğrulama, aramalar, vb. Gibi birçok basit geri arama kullanım demosu içermektedir
PJ Eby

1
@CedricH. Sizin için oy verdi. "Geri arama" gerçek bir kavram mıdır, yoksa "birinci sınıf bir işlev" midir? "Başka bir soru olarak sormak iyi bir soru mu?
prosody-Gab Vereable Bağlam

"Belirli bir olay tetiklendikten sonra geri çağrılan bir işlev" anlamına gelen geri çağrıyı anlıyorum. Bu doğru mu?
JohnDoea

Yanıtlar:


44

Tipik zorunlu programlamada , talimat dizileri yazarsınız ve bunlar açık kontrol akışı ile birbiri ardına yürütülür. Örneğin:

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

vb.

Örnekte görülebileceği gibi, zorunlu programlamada yürütme akışını kolayca takip edersiniz, verdiğiniz herhangi bir komutun bir sonucu olarak yürütüleceğini bilerek, yürütme bağlamını belirlemek için her zaman herhangi bir kod satırından yukarı doğru ilerlersiniz. akıştaki konum (veya işlevleri yazıyorsanız arama sitelerinin konumlarını).

Geri aramalar akışı nasıl değiştirir?

Geri aramalar kullandığınızda, bir dizi talimatı "coğrafi olarak" kullanmak yerine, ne zaman çağrılacağını açıklarsınız. Diğer programlama ortamlarındaki tipik örnekler, “bu kaynağı indir ve indirme tamamlandığında bu geri aramayı çağırın” gibi durumlardır. Bash'in bu türden genel bir geri arama yapısı yoktur, ancak hata yönetimi ve diğer bazı durumlar için geri çağrıları vardır ; örneğin (bu örneği anlamak için önce komut değiştirme ve Bash çıkış modlarını anlamak gerekir):

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

Bunu kendiniz denemek istiyorsanız, yukarıdakileri bir dosyaya kaydedin, diyelim ki cleanUpOnExit.shçalıştırılabilir yapın ve çalıştırın:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Buradaki kodum asla açıkça cleanupişlevi çağırmaz; Bash'e ne zaman çağrılacağını söyler trap cleanup EXIT, yani “sevgili Bash, lütfen cleanupçıktığınızda komutu çalıştırın ” (ve cleanupdaha önce tanımladığım bir işlev olur, ancak Bash'in anladığı her şey olabilir). Bash bunu tüm ölümcül olmayan sinyaller, çıkışlar, komut hataları ve genel hata ayıklama için destekler (her komuttan önce çalıştırılacak bir geri arama belirleyebilirsiniz). Buradaki geri arama cleanup, Bash tarafından kabuk çıkmadan hemen önce "geri çağrılan" işlevdir.

Bash'in kabuk parametrelerini komut olarak değerlendirme yeteneğini, geri arama odaklı bir çerçeve oluşturmak için kullanabilirsiniz; bu, bu cevabın kapsamının biraz ötesindedir ve etrafta dolaşmanın her zaman geri aramalar içerdiğini düşündürerek belki de daha fazla karışıklığa neden olur. Temel işleve bazı örnekler için bkz. Bash: bir işlevi parametre olarak iletme. Buradaki fikir, olay işleme geri çağrılarında olduğu gibi, işlevlerin verileri parametre olarak alabileceği gibi, diğer işlevler de - bu, arayanların verilerin yanı sıra davranış sağlamasına da izin verir. Bu yaklaşımın basit bir örneği,

#!/bin/bash

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

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

doonall backup "$@"

( cpBirden fazla dosyayla başa çıkabildiğinden, bunun biraz işe yaramaz olduğunu biliyorum , sadece örnekleme amaçlıdır.)

Burada doonallparametre olarak verilen başka bir komutu alan ve parametrelerinin geri kalanına uygulayan bir fonksiyon yaratıyoruz ; backupbunu, betiğe verilen tüm parametrelerde işlevi çağırmak için kullanırız . Sonuç, tüm bağımsız değişkenlerini birer birer yedek dizine kopyalayan bir komut dosyasıdır.

Bu tür bir yaklaşım, işlevlerin tek sorumluluklarla yazılmasına izin verir: doonall'nin sorumluluğu, tüm argümanlarında birer birer birer şey yürütmektir; backup'nin sorumluluğu, (bağımsız) argümanının bir kopyasını bir yedekleme dizininde oluşturmaktır. Her ikisi de doonallve backupdaha fazla kod yeniden kullanımı, daha iyi testler vb. Sağlayan diğer bağlamlarda kullanılabilir.

Bu durumda geri arama, diğer bağımsız değişkenlerinin her birinde “geri çağırmayı” backupsöylediğimiz işlevdir doonall- doonalldavranışla (ilk argümanını) ve verileri (kalan argümanları) sunarız.

(İkinci örnekte gösterilen türden bir kullanım örneğinde, “geri arama” terimini kendim kullanmayacağımı, ancak belki de kullandığım dillerden kaynaklanan bir alışkanlık olduğunu düşünüyorum. geri aramaları olay odaklı bir sisteme kaydetmek yerine)


25

İlk olarak, bir işlevi geri arama işlevi yapan şeyin ne yaptığının değil, nasıl kullanıldığına dikkat etmek önemlidir. Geri arama, yazdığınız kod, yazmadığınız koddan çağrıldığındadır. Belirli bir olay meydana geldiğinde sistemden sizi geri aramasını istiyorsunuz.

Kabuk programlamasında geri arama örneği tuzaklardır. Tuzak, işlev olarak ifade edilmeyen, ancak değerlendirilecek bir kod parçası olarak geri çağrıdır. Kabuk belirli bir sinyal aldığında kabuktan kodunuzu çağırmasını istiyorsunuz.

Geri aramanın başka bir örneği -exec, findkomutun eylemidir . findKomutun işi dizinleri tekrar tekrar dolaşmak ve her dosyayı sırayla işlemektir. Varsayılan olarak, işlem dosya adını (örtük -print) yazdırmaktır , ancak işlemle -execbelirttiğiniz bir komutu çalıştırmaktır. Bu, geri arama tanımına uyar, ancak geri aramalar devam ederken, geri arama ayrı bir işlemde çalıştığından çok esnek değildir.

Benzer bir işlev uyguladıysanız, her dosyayı çağırmak için bir geri çağırma işlevi kullanmasını sağlayabilirsiniz. Burada, işlev adını (veya harici komut adını) bağımsız değişken olarak alan ve onu geçerli dizindeki ve alt dizinlerindeki tüm normal dosyalarda çağıran ultra basitleştirilmiş bir benzer işlev. İşlev, her call_on_regular_filesnormal dosya bulduğunda çağrılan bir geri arama olarak kullanılır .

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

Geri aramalar kabuk programlamasında diğer bazı ortamlarda olduğu kadar yaygın değildir, çünkü kabuklar temel olarak basit programlar için tasarlanmıştır. Geri aramalar, veri ve kontrol akışının bağımsız olarak yazılan ve dağıtılan kod bölümleri arasında ileri geri hareket etme olasılığının daha yüksek olduğu ortamlarda daha yaygındır: temel sistem, çeşitli kütüphaneler, uygulama kodu.


1
Özellikle güzel açıkladı
roaima

1
@JohnDoea Bence fikir, gerçekten yazacağınız bir işlev olmadığı için çok basitleştirilmiş olmasıdır. Ama belki daha basit bir örnek üzerinde geri arama çalışmasına kodlanmış liste ile bir şey olurdu: 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; }Eğer olarak çalıştırmak hangi foreach_server echo, foreach_server nslookupvb declare callback="$1"yaklaşık olsa alabilirsiniz basit olarak: geri arama yerde de geçirilecek olan, ya da geri arama değildir.
IMSoP

4
'Geri arama, yazdığınız kod, yazmadığınız koddan çağrıldığındadır.' sadece yanlış. Bazı engelleme olmayan eşzamansız işleri yapan bir şey yazabilir ve tamamlandığında çalışacak bir geri arama ile çalıştırabilirsiniz. Hiçbir şey kodu kimin
yazdığıyla

5
@mikemaccana Tabii ki aynı kişi kodun iki parçasını da yazmış olabilir. Ancak bu yaygın bir durum değildir. Resmi bir tanım vermemek yerine, bir kavramın temellerini açıklıyorum. Tüm köşe vakalarını açıklarsanız, temelleri aktarmak zordur.
Gilles 'SO- kötü olmayı bırak'

1
Duyduğuma sevindim. Ben hem geri arama kullanan hem de geri arama kullanan kod yazma insanların yaygın olmadığını veya bir uç durumda olduğunu ve karışıklık nedeniyle, bu cevabın temelleri ilettiğini kabul etmiyorum.
mikemaccana

7

"geri çağrılar" sadece diğer işlevlere argüman olarak iletilen işlevlerdir.

Kabuk düzeyinde bu, diğer komut dosyalarına / fonksiyonlara / komutlara argüman olarak iletilen komut dosyaları / fonksiyonlar / komutlar anlamına gelir.

Şimdi, basit bir örnek için, aşağıdaki komut dosyasını düşünün:

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

özetin olması

x command filter [file ...]

filterher filebağımsız değişken için geçerli olacak , ardından commandfiltrelerin çıktıları ile bağımsız değişken olarak çağrılacaktır.

Örneğin:

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

Bu lisp'te neler yapabileceğinize çok yakın (sadece şaka ;-))

Bazı insanlar "geri arama" terimini "olay işleyici" ve / veya "kapatma" ile sınırlamakta ısrar ederler (işlev + veri / çevre grubu); bu hiçbir şekilde genel kabul gören anlam değildir. Ve bu dar duyulardaki "geri aramaların" kabukta çok fazla kullanılmamasının bir nedeni, borular + paralellik + dinamik programlama yeteneklerinin çok daha güçlü olması ve performans açısından ödeme yapıyorsanız bile, kabuğu tıknaz bir sürümü olarak kullanmaya çalışın perlveya python.


Örneğiniz oldukça kullanışlı görünse de, nasıl çalıştığını anlamak için bash manuel açık ile gerçekten ayırmam gerekecek kadar yoğun (ve yıllarca daha basit bash ile çalıştım.) Asla öğrenmedim lisp. ;)
Joe

1
@Joe sadece iki giriş dosyaları ve hiçbir ile işe 's Tamam eğer %filtrelerde interpolasyon, bütün her şey düşürüldü olabilir: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Ama bu çok daha az kullanışlı ve açıklayıcı imho.
mosvy

1
Ya da daha iyisi$1 <($2 "$3") <($2 "$4")
mosvy

+1 Teşekkürler. Yorumlarınız, ona bakmak ve bir süre kodla oynamak, benim için açıklığa kavuşturdu. Sonsuza kadar kullandığım bir şey için yeni bir "string interpolation" terimi de öğrendim.
Joe

4

Türü.

Bash'da geri arama yapmanın basit bir yolu, bir programın adını "geri arama işlevi" olarak işlev gören bir parametre olarak kabul etmektir.

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

Bu şu şekilde kullanılır:

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

Tabii ki bash'te kapaklarınız yok. Bu nedenle, geri arama işlevinin arayan tarafındaki değişkenlere erişimi yoktur. Bununla birlikte, geri arama için gereken verileri ortam değişkenlerinde saklayabilirsiniz. Geri aramadan invoker betiğine bilgi iletmek daha zordur. Veriler bir dosyaya yerleştirilebilir.

Tasarımınız her şeyin tek bir işlemde işlenmesine izin veriyorsa, geri arama için bir kabuk işlevi kullanabilirsiniz ve bu durumda geri arama işlevi elbette invoker tarafındaki değişkenlere erişebilir.


3

Sadece diğer cevaplara birkaç kelime eklemek için. Geri arama işlevi, geri çağıran işlevin dışındaki işlev (ler) de çalışır. Bunun mümkün olması için, geri çağrılacak işlevin tam bir tanımının geri çağrılan işleve geçirilmesi gerekir veya kodu geri çağrılan işleve açık olmalıdır.

Eski (kodu başka bir işleve geçirme) mümkündür, ancak bunun için bir örnek atlayacağım, bunun karmaşıklığı içerecektir. İkincisi (işlevi ada göre geçirme) yaygın bir uygulamadır, çünkü bir işlevin kapsamı dışında bildirilen değişkenler ve işlevler, tanımları üzerlerinde çalışan işleve çağrıdan önce geldiği sürece bu işlevde kullanılabilir. olarak adlandırılır).

Ayrıca, işlevler dışa aktarıldığında benzer bir şey olduğunu unutmayın. Bir işlevi içe aktaran bir kabuk hazır bir çerçeveye sahip olabilir ve sadece işlev tanımlarının onları harekete geçirmesini bekleyebilir. İşlev dışa aktarımı Bash'te bulunur ve daha önce ciddi sorunlara neden olur, btw (Shellshock olarak adlandırılır):

Bu yanıtı, Bash'te açıkça bulunmayan bir işlevi başka bir işleve geçirmenin bir yöntemi ile tamamlayacağım. Bu ad ile değil, adresle geçiyor. Bu, örneğin Perl'de bulunabilir. Bash ne fonksiyonlar ne de değişkenler için bu yolu sunar. Ancak, belirttiğiniz gibi, Bash ile sadece bir örnek olarak daha geniş bir resme sahip olmak istiyorsanız, o zaman bilmelisiniz ki, işlev kodu bellekte bir yerde bulunabilir ve bu koda erişilen bellek konumu tarafından erişilebilir. adresini aradı.


2

Bash'taki geri aramanın en basit örneklerinden biri, birçok insanın aşina olduğu ancak aslında hangi tasarım desenini kullandıklarının farkında değil:

cron

Cron, bazı koşullar yerine getirildiğinde (zaman belirtimi) cron programının geri arayacağı bir yürütülebilir (ikili veya komut dosyası) belirtmenize olanak tanır

Diyelim ki senaryonuz var doEveryDay.sh. Komut dosyasını yazmanın geri çağırmayan yolu:

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

Yazmanın geri çağırma yolu basitçe:

#! /bin/bash
doSomething

Sonra crontab'da şöyle bir şey belirlersiniz

0 0 * * *     doEveryDay.sh

Daha sonra etkinliğin tetiklenmesini beklemek için kodu yazmanız gerekmez, bunun yerine cronkodunuzu geri çağırmanız gerekir.


Şimdi, bu kodu NASIL yazacağınızı düşünün.

Bash'da başka bir komut dosyasını / işlevi nasıl yürütürdünüz?

Bir işlev yazalım:

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
}

Şimdi geri aramayı kabul eden bir işlev oluşturdunuz. Sadece şöyle diyebilirsiniz:

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

Tabii ki, every24hours işlevi asla geri dönmez. Bash, onu kolayca eşzamansız hale getirebileceğimiz ve ekleyerek bir süreç oluşturabileceğimiz için biraz benzersizdir &:

every24hours 'curl google.com' &

Bunu bir işlev olarak istemiyorsanız, bunu bir komut dosyası olarak yapabilirsiniz:

#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

Gördüğünüz gibi, bash'da geri aramalar önemsizdir. Basitçe:

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

Ve geri aramayı aramak basitçe:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Yukarıdaki formu görebileceğiniz gibi, geri aramalar nadiren doğrudan dillerin özellikleridir. Genellikle mevcut dil özelliklerini kullanarak yaratıcı bir şekilde programlama yaparlar. Bazı kod bloğunun / fonksiyonun / betiğin bir göstergesini / referansını / kopyasını saklayabilen herhangi bir dil geri çağrıları uygulayabilir.


Geri çağrıları kabul eden diğer program / komut dosyası örnekleri watchve find( -execparametre ile kullanıldığında )
slebetman 23:18

0

Geri arama, bir olay meydana geldiğinde çağrılan bir işlevdir. Buradaki bashtek olay işleme mekanizması, sinyaller, kabuk çıkışı ve kabuk hataları olayları, hata ayıklama olayları ve işlev / kaynaklı komut dosyaları dönüş olayları ile ilgilidir.

İşte işe yaramaz ama basit bir geri arama kaldıraç sinyal tuzaklarına bir örnek.

Önce geri aramayı uygulayan komut dosyasını oluşturun:

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

Ardından komut dosyasını bir terminalde çalıştırın:

$ ./callback-example

ve bir diğerinde USR1sinyali kabuk işlemine gönderin.

$ pkill -USR1 callback-example

Gönderilen her sinyal, ilk terminalde aşağıdakine benzer hatların görüntülenmesini tetiklemelidir:

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

ksh93, bashdaha sonra benimsenen birçok özelliği uygulayan kabuk olarak "disiplin işlevleri" adını verdiği şeyi sağlar. Kullanılamayan bu işlevler, bashbir kabuk değişkeni değiştirildiğinde veya referans verildiğinde (yani okunur) çağrılır. Bu olay odaklı uygulamalar için daha ilginç bir yol açar.

Örneğin, bu özellik, grafik widget'larındaki X11 / Xt / Motif stili geri çağrıların, kshadı verilen grafik uzantılarının eski bir sürümünde uygulanmasına izin verdi dtksh. Dksh kılavuzuna bakın .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.