Apakah konsep "panggilan balik" pemrograman ada di Bash?

21

Beberapa kali ketika saya membaca tentang pemrograman saya menemukan konsep "panggilan balik".

Lucunya, saya tidak pernah menemukan penjelasan yang bisa saya sebut "didaktik" atau "jelas" untuk istilah "fungsi panggilan balik" ini (hampir semua penjelasan yang saya baca tampak cukup berbeda dari yang lain dan saya merasa bingung).

Apakah konsep pemrograman "panggilan balik" ada di Bash? Jika demikian, harap jawab dengan contoh kecil, sederhana, Bash.

JohnDoea
sumber
2
Apakah "panggilan balik" adalah konsep aktual atau apakah itu "fungsi kelas satu"?
Cedric H.
Anda mungkin menemukan hal declarative.bashmenarik, sebagai kerangka kerja yang secara eksplisit memanfaatkan fungsi yang dikonfigurasikan untuk dipanggil ketika nilai yang diberikan diperlukan.
Charles Duffy
Kerangka kerja lain yang relevan: bashup / acara . Dokumentasinya mencakup banyak demo penggunaan callback yang sederhana, seperti untuk validasi, pencarian, dll.
PJ Eby
1
@CedricH. Dipilih untuk Anda. "Apakah" panggilan balik "adalah konsep aktual atau apakah itu" fungsi kelas satu "?" Adalah pertanyaan yang bagus untuk diajukan sebagai pertanyaan lain?
prosody-Gab Vereable Konteks
Saya mengerti panggilan balik berarti "fungsi yang dipanggil kembali setelah peristiwa yang diberikan dipicu". Apakah itu benar?
JohnDoea

Jawaban:

44

Dalam pemrograman imperatif tipikal , Anda menulis urutan instruksi dan mereka dieksekusi satu demi satu, dengan aliran kontrol eksplisit. Sebagai contoh:

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

dll.

Seperti yang dapat dilihat dari contoh, dalam pemrograman imperatif Anda mengikuti alur eksekusi dengan cukup mudah, selalu bekerja dengan cara Anda dari setiap baris kode tertentu untuk menentukan konteks eksekusi, mengetahui bahwa setiap instruksi yang Anda berikan akan dieksekusi sebagai hasil dari perintah mereka. lokasi di aliran (atau lokasi situs panggilan mereka, jika Anda menulis fungsi).

Bagaimana panggilan balik mengubah alur

Saat Anda menggunakan panggilan balik, alih-alih menempatkan penggunaan serangkaian instruksi "secara geografis", Anda menjelaskan kapan harus dipanggil. Contoh umum dalam lingkungan pemrograman lain adalah kasus seperti "unduh sumber ini, dan ketika unduhan selesai, panggil panggilan balik ini". Bash tidak memiliki konstruk panggilan balik generik dari jenis ini, tetapi ia memiliki panggilan balik, untuk penanganan kesalahan dan beberapa situasi lainnya; misalnya (kita harus terlebih dahulu memahami substitusi perintah dan mode keluar Bash untuk memahami contoh itu):

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

Jika Anda ingin mencoba ini sendiri, simpan di atas dalam file, katakan cleanUpOnExit.sh, buat itu dapat dieksekusi dan jalankan:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Kode saya di sini tidak pernah secara eksplisit memanggil cleanupfungsi; ia memberi tahu Bash kapan harus memanggilnya, menggunakan trap cleanup EXIT, yaitu "Bash sayang, tolong jalankan cleanupperintah ketika Anda keluar" (dan cleanupkebetulan merupakan fungsi yang saya definisikan sebelumnya, tapi itu bisa apa saja yang dipahami Bash). Bash mendukung ini untuk semua sinyal non-fatal, keluar, kegagalan perintah, dan debugging umum (Anda dapat menentukan panggilan balik yang dijalankan sebelum setiap perintah). Callback di sini adalah cleanupfungsi, yang "dipanggil kembali" oleh Bash tepat sebelum shell keluar.

Anda dapat menggunakan kemampuan Bash untuk mengevaluasi parameter shell sebagai perintah, untuk membangun kerangka kerja berorientasi-panggilan balik; itu agak di luar ruang lingkup jawaban ini, dan mungkin akan menyebabkan lebih banyak kebingungan dengan menyarankan bahwa fungsi lewat di sekitar selalu melibatkan panggilan balik. Lihat Bash: meneruskan fungsi sebagai parameter untuk beberapa contoh fungsionalitas yang mendasarinya. Idenya di sini, seperti halnya callback penanganan event, adalah bahwa fungsi dapat mengambil data sebagai parameter, tetapi juga fungsi lainnya - ini memungkinkan penelepon untuk memberikan perilaku serta data. Contoh sederhana dari pendekatan ini bisa terlihat

#!/bin/bash

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

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

doonall backup "$@"

(Saya tahu ini agak tidak berguna karena cpdapat menangani banyak file, hanya untuk ilustrasi.)

Di sini kita membuat fungsi,, doonallyang mengambil perintah lain, diberikan sebagai parameter, dan menerapkannya ke seluruh parameternya; maka kita menggunakannya untuk memanggil backupfungsi pada semua parameter yang diberikan ke skrip. Hasilnya adalah skrip yang menyalin semua argumennya, satu per satu, ke direktori cadangan.

Pendekatan semacam ini memungkinkan fungsi ditulis dengan tanggung jawab tunggal: doonalltanggung jawab adalah menjalankan sesuatu pada semua argumennya, satu per satu; backupTanggung jawabnya adalah membuat salinan argumennya (satu-satunya) di direktori cadangan. Keduanya doonalldan backupdapat digunakan dalam konteks lain, yang memungkinkan lebih banyak penggunaan kembali kode, pengujian yang lebih baik, dll.

Dalam hal ini, callback adalah backupfungsi, yang kita beri tahu doonalluntuk “menelepon kembali” pada masing-masing argumen lainnya - kami menyediakan doonallperilaku (argumen pertama) serta data (argumen yang tersisa).

(Perhatikan bahwa dalam jenis use-case yang ditunjukkan pada contoh kedua, saya tidak akan menggunakan istilah "callback" sendiri, tapi itu mungkin kebiasaan yang dihasilkan dari bahasa yang saya gunakan. Saya menganggap ini sebagai fungsi lewat atau lambda di sekitar , daripada mendaftarkan panggilan balik dalam sistem berorientasi peristiwa.)

Stephen Kitt
sumber
25

Pertama, penting untuk dicatat bahwa apa yang menjadikan suatu fungsi fungsi panggil balik adalah bagaimana fungsi itu digunakan, bukan apa fungsinya. Panggilan balik adalah ketika kode yang Anda tulis dipanggil dari kode yang tidak Anda tulis. Anda meminta sistem untuk menelepon Anda kembali ketika beberapa peristiwa tertentu terjadi.

Contoh panggilan balik dalam pemrograman shell adalah trap. Jebakan adalah panggilan balik yang tidak dinyatakan sebagai fungsi, tetapi sebagai bagian dari kode untuk dievaluasi. Anda meminta shell untuk memanggil kode Anda ketika shell menerima sinyal tertentu.

Contoh lain dari callback adalah -execaksi dari findperintah. Tugas dari findperintah ini adalah untuk menelusuri direktori secara rekursif dan memproses setiap file secara bergantian. Secara default, pemrosesan adalah untuk mencetak nama file (implisit -print), tetapi dengan -execpemrosesan adalah untuk menjalankan perintah yang Anda tentukan. Ini sesuai dengan definisi panggilan balik, meskipun saat panggilan balik berlangsung, itu tidak terlalu fleksibel karena panggilan balik berjalan dalam proses terpisah.

Jika Anda menerapkan fungsi find-like, Anda bisa membuatnya menggunakan fungsi callback untuk memanggil setiap file. Inilah fungsi mirip-cari yang sangat disederhanakan yang menggunakan nama fungsi (atau nama perintah eksternal) sebagai argumen dan menyebutnya pada semua file biasa di direktori saat ini dan subdirektori. Fungsi ini digunakan sebagai panggilan balik yang dipanggil setiap kali call_on_regular_filesmenemukan file biasa.

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

Callback tidak umum dalam pemrograman shell seperti di beberapa lingkungan lain karena shell terutama dirancang untuk program sederhana. Callback lebih umum di lingkungan di mana data dan aliran kontrol lebih cenderung bergerak bolak-balik antara bagian-bagian dari kode yang ditulis dan didistribusikan secara independen: sistem dasar, berbagai perpustakaan, kode aplikasi.

Gilles 'SANGAT berhenti menjadi jahat'
sumber
1
Terutama dijelaskan dengan baik
roaima
1
@ JohnDoea Saya pikir idenya adalah bahwa ini sangat disederhanakan karena itu bukan fungsi yang akan Anda tulis. Tapi mungkin contoh yang lebih sederhana akan menjadi sesuatu dengan daftar keras-kode untuk menjalankan callback pada: 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; }mana Anda bisa menjalankan sebagai foreach_server echo, foreach_server nslookup, dll declare callback="$1"adalah tentang yang sederhana seperti itu bisa mendapatkan meskipun: callback telah akan disahkan di suatu tempat, atau itu bukan panggilan balik.
IMSoP
4
'Panggilan balik adalah ketika kode yang Anda tulis dipanggil dari kode yang tidak Anda tulis.' itu salah. Anda dapat menulis hal yang berfungsi sebagai async non-pemblokiran, dan jalankan dengan panggilan balik yang akan berjalan saat selesai. Tidak ada yang terkait dengan yang menulis kode,
mikemaccana
5
@mikemaccana Tentu saja mungkin orang yang sama menulis dua bagian kode. Tapi itu bukan kasus umum. Saya menjelaskan dasar-dasar konsep, tidak memberikan definisi formal. Jika Anda menjelaskan semua kasus sudut, sulit untuk menyampaikan dasar-dasarnya.
Gilles 'SO- stop being evil'
1
Senang mendengarnya. Saya tidak setuju bahwa orang-orang menulis kode yang menggunakan callback dan callback tidak umum atau merupakan kasus tepi, dan, karena kebingungan, bahwa jawaban ini menyampaikan dasar-dasarnya.
mikemaccana
7

"panggilan balik" hanyalah fungsi yang diteruskan sebagai argumen ke fungsi lain.

Pada level shell, itu berarti skrip / fungsi / perintah dilewatkan sebagai argumen ke skrip / fungsi / perintah lain.

Sekarang, untuk contoh sederhana, pertimbangkan skrip berikut:

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

memiliki sinopsis

x command filter [file ...]

akan berlaku filteruntuk setiap fileargumen, lalu panggil commanddengan output filter sebagai argumen.

Contohnya:

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

Ini sangat dekat dengan apa yang dapat Anda lakukan di lisp (hanya bercanda ;-))

Beberapa orang bersikeras membatasi istilah "callback" menjadi "event handler" dan / atau "closure" (fungsi + data / tuple lingkungan); ini sama sekali bukan makna yang diterima secara umum . Dan salah satu alasan mengapa "panggilan balik" dalam pengertian sempit itu tidak banyak digunakan dalam shell adalah karena pipa + paralelisme + kemampuan pemrograman dinamis jauh lebih kuat, dan Anda sudah membayar mereka dalam hal kinerja, bahkan jika Anda coba gunakan shell sebagai versi kikuk perlatau python.

mosvy
sumber
Walaupun contoh Anda terlihat cukup bermanfaat, cukup padat sehingga saya harus benar-benar memisahnya dengan manual bash terbuka untuk mengetahui cara kerjanya (dan saya telah bekerja dengan bash yang lebih sederhana hampir setiap hari selama bertahun-tahun.) Saya tidak pernah belajar pelat. ;)
Joe
1
@ Joe jika OK itu untuk bekerja dengan hanya dua input file dan tidak ada %interpolasi di filter, semuanya dapat dikurangi ke: cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Tapi itu jauh lebih berguna dan ilho ilustratif.
Mosvy
1
Atau bahkan lebih baik$1 <($2 "$3") <($2 "$4")
mosvy
+1 Terima kasih. Komentar Anda, ditambah menatapnya dan bermain dengan kode untuk beberapa waktu, mengklarifikasi untuk saya. Saya juga belajar istilah baru, "interpolasi string", untuk sesuatu yang telah saya gunakan selamanya.
Joe
4

Agak.

Salah satu cara sederhana untuk mengimplementasikan panggilan balik di bash, adalah dengan menerima nama program sebagai parameter, yang bertindak sebagai "fungsi panggilan balik".

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

Ini akan digunakan seperti ini:

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

Tentu saja Anda tidak memiliki penutupan di bash. Oleh karena itu, fungsi panggilan balik tidak memiliki akses ke variabel di sisi pemanggil. Namun, Anda dapat menyimpan data yang dibutuhkan panggilan balik dalam variabel lingkungan. Mengirim informasi kembali dari callback ke skrip invoker lebih sulit. Data dapat ditempatkan ke dalam file.

Jika desain Anda memungkinkan semuanya ditangani dalam satu proses tunggal, Anda dapat menggunakan fungsi shell untuk callback, dan dalam hal ini fungsi callback tentu saja memiliki akses ke variabel di sisi invoker.

pengguna1934428
sumber
3

Hanya dengan menambahkan beberapa kata ke jawaban lainnya. Fungsi panggil balik berfungsi pada fungsi eksternal fungsi yang memanggil kembali. Agar hal ini dimungkinkan, baik definisi keseluruhan dari fungsi yang dipanggil kembali perlu diteruskan ke fungsi yang memanggil kembali, atau kodenya harus tersedia untuk fungsi yang memanggil kembali.

Yang pertama (meneruskan kode ke fungsi lain) dimungkinkan, meskipun saya akan melewatkan contoh untuk ini akan melibatkan kompleksitas. Yang terakhir (melewati fungsi dengan nama) adalah praktik umum, karena variabel dan fungsi yang dideklarasikan di luar cakupan satu fungsi tersedia dalam fungsi tersebut selama definisi mereka mendahului panggilan ke fungsi yang beroperasi padanya (yang, pada gilirannya, , sebagaimana akan dinyatakan sebelum dipanggil).

Perhatikan juga, bahwa hal serupa terjadi ketika fungsi diekspor. Shell yang mengimpor fungsi mungkin memiliki kerangka kerja yang siap dan hanya menunggu definisi fungsi untuk menjalankannya. Fungsi ekspor hadir di Bash dan menyebabkan masalah yang sebelumnya serius, btw (yang disebut Shellshock):

Saya akan melengkapi jawaban ini dengan satu metode lagi untuk meneruskan fungsi ke fungsi lain, yang tidak secara eksplisit ada di Bash. Ini melewati alamat, bukan dengan nama. Ini dapat ditemukan di Perl, misalnya. Bash menawarkan cara ini baik untuk fungsi, maupun variabel. Tetapi jika, seperti yang Anda nyatakan, Anda ingin memiliki gambar yang lebih luas dengan Bash hanya sebagai contoh, maka Anda harus tahu, bahwa kode fungsi dapat berada di suatu tempat di memori, dan kode itu dapat diakses oleh lokasi memori itu, yang merupakan menyebut alamatnya.

Tomasz
sumber
2

Salah satu contoh paling sederhana dari callback di bash adalah salah satu yang banyak orang kenal tetapi tidak menyadari pola desain apa yang sebenarnya mereka gunakan:

cron

Cron memungkinkan Anda untuk menentukan yang dapat dieksekusi (biner atau skrip) yang akan dipanggil kembali oleh program cron ketika beberapa kondisi terpenuhi (spesifikasi waktu)

Katakanlah Anda memiliki skrip yang dipanggil doEveryDay.sh. Cara non-callback untuk menulis skrip adalah:

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

Cara panggilan balik untuk menulisnya adalah:

#! /bin/bash
doSomething

Kemudian di crontab Anda akan mengatur sesuatu seperti

0 0 * * *     doEveryDay.sh

Maka Anda tidak perlu menulis kode untuk menunggu acara untuk memicu tetapi sebaliknya mengandalkan cronuntuk memanggil kode Anda kembali.


Sekarang, pertimbangkan BAGAIMANA Anda akan menulis kode ini di bash.

Bagaimana Anda menjalankan skrip / fungsi lain di bash?

Mari kita menulis fungsi:

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
}

Sekarang Anda telah membuat fungsi yang menerima panggilan balik. Anda bisa menyebutnya seperti ini:

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

Tentu saja, fungsi setiap 24 jam tidak pernah kembali. Bash agak unik karena kita dapat dengan mudah membuatnya asinkron dan menelurkan proses dengan menambahkan &:

every24hours 'curl google.com' &

Jika Anda tidak menginginkan ini sebagai fungsi, Anda dapat melakukannya sebagai skrip:

#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

Seperti yang Anda lihat, panggilan balik dalam bash adalah sepele. Ini hanyalah:

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

Dan menelepon panggilan balik itu sederhana:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Seperti yang Anda lihat pada formulir di atas, panggilan balik jarang merupakan fitur langsung dari bahasa. Mereka biasanya pemrograman secara kreatif menggunakan fitur bahasa yang ada. Bahasa apa pun yang dapat menyimpan pointer / referensi / salinan beberapa blok kode / fungsi / skrip dapat menerapkan panggilan balik.

Slebetman
sumber
Contoh lain dari program / skrip yang menerima panggilan balik meliputi watchdan find(bila digunakan dengan -execparameter)
slebetman
0

Panggilan balik adalah fungsi yang dipanggil saat beberapa peristiwa terjadi. Dengan bash, satu-satunya mekanisme penanganan peristiwa di tempat terkait dengan sinyal, keluar shell, dan diperluas ke acara kesalahan shell, acara debug dan fungsi / sumber acara kembali skrip bersumber.

Berikut adalah contoh perangkap sinyal pengungkit balik yang tidak berguna tetapi sederhana.

Pertama buat skrip yang mengimplementasikan callback:

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

Kemudian jalankan skrip dalam satu terminal:

$ ./callback-example

dan pada yang lain, kirim USR1sinyal ke proses shell.

$ pkill -USR1 callback-example

Setiap sinyal yang dikirim harus memicu tampilan garis seperti ini di terminal pertama:

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

ksh93, karena shell mengimplementasikan banyak fitur yang bashkemudian diadopsi, menyediakan apa yang disebutnya "fungsi disiplin". Fungsi-fungsi ini, tidak tersedia dengan bash, dipanggil ketika variabel shell dimodifikasi atau direferensikan (yaitu dibaca). Ini membuka jalan ke aplikasi event driven yang lebih menarik.

Misalnya, fitur ini memungkinkan panggilan balik gaya X11 / Xt / Motif pada widget grafis untuk diimplementasikan dalam versi lama dari kshitu termasuk ekstensi grafik yang disebut dtksh. Lihat manual dksh .

Jlliagre
sumber