Có phải khái niệm về cuộc gọi lại của Wikipedia về lập trình tồn tại ở Bash?


21

Một vài lần khi tôi đọc về lập trình, tôi đã bắt gặp khái niệm "gọi lại".

Vui thay, tôi không bao giờ tìm thấy một lời giải thích mà tôi có thể gọi là "didactic" hoặc "rõ ràng" cho thuật ngữ này "chức năng gọi lại" (hầu như bất kỳ lời giải thích nào tôi đọc dường như đủ khác với tôi và tôi cảm thấy bối rối).

Là khái niệm "gọi lại" của lập trình tồn tại trong Bash? Nếu vậy, xin vui lòng trả lời với một ví dụ Bash nhỏ, đơn giản.


2
"Gọi lại" là một khái niệm thực tế hay nó là "chức năng hạng nhất"?
Cedric H.

Bạn có thể thấy declarative.bashthú vị, vì một khung công tác tận dụng rõ ràng các chức năng được cấu hình để được gọi khi cần một giá trị nhất định.
Charles Duffy

Một khung có liên quan khác: bashup / sự kiện . Tài liệu của nó bao gồm rất nhiều bản demo đơn giản về sử dụng gọi lại, chẳng hạn như để xác thực, tra cứu, v.v.
PJ Eby

1
@CedricH. Bình chọn cho bạn. "Là" gọi lại "là một khái niệm thực tế hay nó là" chức năng hạng nhất "?" Là một câu hỏi hay để hỏi như một câu hỏi khác?
bối cảnh thuận lợi-Gab có thể xác minh được vào

Tôi hiểu gọi lại có nghĩa là "một chức năng được gọi lại sau khi một sự kiện nhất định được kích hoạt". Đúng không?
JohnDoea

Câu trả lời:


44

Trong lập trình mệnh lệnh điển hình , bạn viết các chuỗi hướng dẫn và chúng được thực hiện lần lượt, với luồng điều khiển rõ ràng. Ví dụ:

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

v.v.

Như có thể thấy từ ví dụ này, trong lập trình mệnh lệnh, bạn tuân theo luồng thực thi khá dễ dàng, luôn luôn làm việc theo cách của bạn từ bất kỳ dòng mã đã cho nào để xác định bối cảnh thực thi của nó, biết rằng bất kỳ lệnh nào bạn đưa ra sẽ được thực hiện do kết quả của chúng vị trí trong luồng (hoặc vị trí trang web cuộc gọi của họ, nếu bạn đang viết chức năng).

Cách gọi lại thay đổi dòng chảy

Khi bạn sử dụng các cuộc gọi lại, thay vì đặt việc sử dụng một bộ hướng dẫn, về mặt địa lý, bạn sẽ mô tả khi nào nên gọi nó. Các ví dụ điển hình trong các môi trường lập trình khác là các trường hợp như tải xuống tài nguyên này và khi quá trình tải xuống hoàn tất, hãy gọi đây là cuộc gọi lại. Bash không có cấu trúc gọi lại chung loại này, nhưng nó có các cuộc gọi lại, để xử lý lỗi và một vài tình huống khác; ví dụ (trước tiên người ta phải hiểu thay thế lệnhchế độ thoát Bash để hiểu ví dụ đó):

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

Nếu bạn muốn tự mình thử điều này, hãy lưu phần trên vào một tệp, giả sử cleanUpOnExit.sh, làm cho nó có thể thực thi được và chạy nó:

chmod 755 cleanUpOnExit.sh
./cleanUpOnExit.sh

Mã của tôi ở đây không bao giờ gọi cleanuphàm một cách rõ ràng ; nó nói với Bash khi nào nên gọi nó, bằng cách sử dụng trap cleanup EXIT, tức là Bash thân yêu Bash, vui lòng chạy cleanuplệnh khi bạn thoát ra (và cleanuptình cờ là một hàm tôi đã xác định trước đó, nhưng nó có thể là bất cứ điều gì Bash hiểu). Bash hỗ trợ điều này cho tất cả các tín hiệu không nghiêm trọng, thoát, lỗi lệnh và gỡ lỗi chung (bạn có thể chỉ định một cuộc gọi lại được chạy trước mỗi lệnh). Hàm gọi lại ở đây là cleanuphàm, được gọi là trở lại bởi Bash bởi Bash ngay trước khi shell thoát ra.

Bạn có thể sử dụng khả năng của Bash để đánh giá các tham số shell dưới dạng các lệnh, để xây dựng khung theo hướng gọi lại; điều đó hơi vượt quá phạm vi của câu trả lời này, và có lẽ sẽ gây ra nhiều nhầm lẫn hơn bằng cách đề xuất rằng việc truyền các hàm xung quanh luôn liên quan đến các cuộc gọi lại. Xem Bash: truyền một hàm làm tham số cho một số ví dụ về chức năng cơ bản. Ý tưởng ở đây, như với các cuộc gọi lại xử lý sự kiện, là các hàm có thể lấy dữ liệu làm tham số, nhưng cũng có các hàm khác - điều này cho phép người gọi cung cấp hành vi cũng như dữ liệu. Một ví dụ đơn giản của phương pháp này có thể trông giống như

#!/bin/bash

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

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

doonall backup "$@"

(Tôi biết điều này hơi vô dụng vì cpcó thể xử lý nhiều tệp, nó chỉ mang tính minh họa.)

Ở đây chúng tôi tạo ra một chức năng, doonall lấy một lệnh khác, được đưa ra làm tham số và áp dụng nó cho các tham số còn lại của nó; sau đó chúng ta sử dụng hàm đó để gọi backuphàm trên tất cả các tham số được cung cấp cho tập lệnh. Kết quả là một tập lệnh sao chép tất cả các đối số của nó, từng cái một, vào một thư mục sao lưu.

Cách tiếp cận này cho phép các chức năng được viết với các trách nhiệm duy nhất: doonalltrách nhiệm của chúng là chạy một cái gì đó trên tất cả các đối số của nó, từng cái một; backupTrách nhiệm của bạn là tạo một bản sao của đối số (duy nhất) của nó trong một thư mục sao lưu. Cả haidoonallbackupcó thể được sử dụng trong các bối cảnh khác, cho phép sử dụng lại nhiều mã hơn, kiểm tra tốt hơn, v.v.

Trong trường hợp này, gọi lại là backupchức năng, mà chúng tôi nóidoonall với Gọi lại gọi lại trên mỗi đối số khác của nó - chúng ta cung cấp doonallhành vi (đối số đầu tiên của nó) cũng như dữ liệu (các đối số còn lại).

(Lưu ý rằng trong trường hợp sử dụng được thể hiện trong ví dụ thứ hai, tôi sẽ không sử dụng thuật ngữ Gọi lại chính mình, nhưng đó có lẽ là thói quen xuất phát từ các ngôn ngữ tôi sử dụng. Tôi nghĩ rằng đây là việc truyền các hàm hoặc lambdas xung quanh , thay vì đăng ký cuộc gọi lại trong một hệ thống hướng sự kiện.)


25

Đầu tiên, điều quan trọng cần lưu ý là những gì làm cho một chức năng gọi lại là cách nó được sử dụng, chứ không phải những gì nó làm. Cuộc gọi lại là khi mã mà bạn viết được gọi từ mã mà bạn không viết. Bạn đang yêu cầu hệ thống gọi lại cho bạn khi có sự kiện cụ thể xảy ra.

Một ví dụ về gọi lại trong lập trình shell là bẫy. Một cái bẫy là một cuộc gọi lại không được biểu thị như một chức năng, mà là một đoạn mã để đánh giá. Bạn đang yêu cầu shell gọi mã của bạn khi shell nhận được tín hiệu cụ thể.

Một ví dụ khác về một cuộc gọi lại là -exechành động của findlệnh. Công việc của findlệnh là duyệt qua các thư mục theo cách đệ quy và xử lý lần lượt từng tệp. Theo mặc định, việc xử lý là in tên tệp (ẩn -print), nhưng với -execviệc xử lý là chạy một lệnh mà bạn chỉ định. Điều này phù hợp với định nghĩa của một cuộc gọi lại, mặc dù tại cuộc gọi lại, nó không linh hoạt vì cuộc gọi lại chạy trong một quy trình riêng biệt.

Nếu bạn đã triển khai một chức năng giống như tìm kiếm, bạn có thể làm cho nó sử dụng chức năng gọi lại để gọi trên mỗi tệp. Đây là một hàm tìm kiếm cực kỳ đơn giản, lấy tên hàm (hoặc tên lệnh bên ngoài) làm đối số và gọi nó trên tất cả các tệp thông thường trong thư mục hiện tại và các thư mục con của nó. Hàm được sử dụng như một cuộc gọi lại được gọi mỗi lần call_on_regular_filestìm thấy một tệp thông thường.

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

Callbacks không phổ biến trong lập trình shell như trong một số môi trường khác vì shell được thiết kế chủ yếu cho các chương trình đơn giản. Các cuộc gọi lại phổ biến hơn trong các môi trường nơi dữ liệu và luồng điều khiển có nhiều khả năng di chuyển qua lại giữa các phần của mã được viết và phân phối độc lập: hệ thống cơ sở, các thư viện khác nhau, mã ứng dụng.


1
Đặc biệt giải thích độc đáo
roaima

1
@JohnDoea Tôi nghĩ ý tưởng là nó cực kỳ đơn giản ở chỗ nó không phải là một chức năng mà bạn thực sự viết. Nhưng có lẽ một ví dụ đơn giản thậm chí sẽ là một cái gì đó với một danh sách mã hóa cứng để chạy các callback trên: 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; }mà bạn có thể chạy như foreach_server echo, foreach_server nslookupvv Các declare callback="$1"khoảng đơn giản như nó có thể nhận được mặc dù: gọi lại đã được thông qua ở nơi nào đó, hoặc nó không phải là một cuộc gọi lại.
IMSoP

4
'Cuộc gọi lại là khi mã mà bạn viết được gọi từ mã mà bạn không viết.' đơn giản là sai. Bạn có thể viết một thứ mà một số async không chặn hoạt động và chạy nó với một cuộc gọi lại nó sẽ chạy khi hoàn thành. Không có gì liên quan đến người đã viết mã,
mikemaccana

5
@mikemaccana Tất nhiên có thể cùng một người đã viết hai phần của mã. Nhưng nó không phải là trường hợp phổ biến. Tôi đang giải thích những điều cơ bản của một khái niệm, không đưa ra một định nghĩa chính thức. Nếu bạn giải thích tất cả các trường hợp góc, thật khó để truyền đạt những điều cơ bản.
Gilles 'SO- ngừng trở nên xấu xa'

1
Vui mừng khi nghe nó. Tôi không đồng ý rằng mọi người viết cả mã sử dụng một cuộc gọi lại và cuộc gọi lại không phổ biến hoặc là một trường hợp cạnh, và vì nhầm lẫn, câu trả lời này truyền tải những điều cơ bản.
mikemaccana

7

"Gọi lại" chỉ là các hàm được truyền dưới dạng đối số cho các hàm khác.

Ở cấp độ vỏ, điều đó chỉ đơn giản có nghĩa là các tập lệnh / hàm / lệnh được truyền dưới dạng đối số cho các tập lệnh / hàm / lệnh khác.

Bây giờ, với một ví dụ đơn giản, hãy xem xét đoạn script sau:

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

có bản tóm tắt

x command filter [file ...]

sẽ áp dụng filtercho từng fileđối số, sau đó gọi commandvới đầu ra của các bộ lọc dưới dạng đối số.

Ví dụ:

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

Điều này rất gần với những gì bạn có thể làm trong lisp (chỉ đùa thôi ;-))

Một số người khăng khăng giới hạn thuật ngữ "gọi lại" đối với "xử lý sự kiện" và / hoặc "đóng cửa" (bộ dữ liệu + dữ liệu / môi trường); Đây không phải là ý nghĩa được chấp nhận chung . Và một lý do tại sao "gọi lại" trong các giác quan hẹp đó không được sử dụng nhiều trong vỏ là vì các đường ống + song song + khả năng lập trình động mạnh mẽ hơn rất nhiều và bạn đã trả tiền cho chúng về mặt hiệu suất, ngay cả khi bạn cố gắng sử dụng shell như một phiên bản hay của .perlpython


Mặc dù ví dụ của bạn trông khá hữu ích, nhưng nó đủ dày đặc mà tôi phải thực sự chọn nó với hướng dẫn bash mở để tìm ra cách nó hoạt động (và tôi đã làm việc với bash đơn giản hơn nhiều ngày trong nhiều năm.) Tôi chưa bao giờ học lisp ;)
Joe

1
@Joe nếu nó hoạt động chỉ với hai tệp đầu vào và không có %nội suy trong các bộ lọc, toàn bộ điều có thể được giảm xuống thành : cmd=$1; shift; flt=$1; shift; $cmd <($flt "$1") <($flt "$2"). Nhưng đó là rất ít hữu ích và minh họa imho.
mosvy

1
Hoặc thậm chí tốt hơn$1 <($2 "$3") <($2 "$4")
mosvy

+1 Cảm ơn. Nhận xét của bạn, cộng với việc nhìn chằm chằm vào nó và chơi với mã trong một thời gian, đã làm rõ nó cho tôi. Tôi cũng đã học được một thuật ngữ mới, "nội suy chuỗi", cho một thứ mà tôi đã sử dụng mãi mãi.
Joe

4

Loại.

Một cách đơn giản để thực hiện gọi lại trong bash, là chấp nhận tên của chương trình làm tham số, hoạt động như "hàm gọi lại".

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

Điều này sẽ được sử dụng như thế này:

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

Tất nhiên bạn không có đóng cửa trong bash. Do đó, hàm gọi lại không có quyền truy cập vào các biến ở phía người gọi. Tuy nhiên, bạn có thể lưu trữ dữ liệu nhu cầu gọi lại trong các biến môi trường. Truyền thông tin trở lại từ cuộc gọi lại cho kịch bản invoker là khó khăn hơn. Dữ liệu có thể được đặt vào một tập tin.

Nếu thiết kế của bạn cho phép mọi thứ được xử lý trong một quy trình duy nhất, bạn có thể sử dụng hàm shell cho hàm gọi lại và trong trường hợp này, hàm gọi lại dĩ nhiên có quyền truy cập vào các biến ở phía bên gọi.


3

Chỉ cần thêm một vài từ cho các câu trả lời khác. Hàm gọi lại hoạt động trên (các) chức năng bên ngoài chức năng gọi lại. Để điều này có thể hoặc toàn bộ định nghĩa của hàm được gọi lại cần phải được chuyển đến hàm gọi lại hoặc mã của nó phải có sẵn cho hàm gọi lại.

Cái trước (truyền mã cho hàm khác) là có thể, mặc dù tôi sẽ bỏ qua một ví dụ cho điều này sẽ liên quan đến sự phức tạp. Cái sau (truyền hàm theo tên) là một cách thông thường, vì các biến và hàm được khai báo bên ngoài phạm vi của một hàm có sẵn trong hàm đó miễn là định nghĩa của chúng đi trước lệnh gọi đến hàm hoạt động trên chúng (lần lượt , như được khai báo trước khi nó được gọi).

Cũng lưu ý rằng, một điều tương tự xảy ra khi các chức năng được xuất khẩu. Một shell nhập một hàm có thể có một khung sẵn sàng và chỉ chờ các định nghĩa hàm đưa chúng vào hoạt động. Xuất khẩu hàm có mặt ở Bash và gây ra các vấn đề nghiêm trọng trước đây, btw (được gọi là Shellshock):

Tôi sẽ hoàn thành câu trả lời này với một phương thức nữa để chuyển một hàm sang một hàm khác, không được trình bày rõ ràng trong Bash. Đây là một trong những địa chỉ, không phải bằng tên. Điều này có thể được tìm thấy trong Perl, ví dụ. Bash cung cấp cách này không cho các hàm, cũng không phải các biến. Nhưng nếu, như bạn nêu, bạn muốn có một hình ảnh rộng hơn với Bash chỉ là một ví dụ, thì bạn nên biết rằng mã chức năng có thể nằm ở đâu đó trong bộ nhớ và mã đó có thể được truy cập bởi vị trí bộ nhớ đó, đó là gọi là địa chỉ của nó.


2

Một trong những ví dụ đơn giản nhất về gọi lại trong bash là một trong số rất nhiều người quen thuộc nhưng không nhận ra mẫu thiết kế nào họ thực sự đang sử dụng:

cron

Cron cho phép bạn chỉ định một tệp thực thi (nhị phân hoặc tập lệnh) mà chương trình cron sẽ gọi lại khi một số điều kiện được đáp ứng (đặc tả thời gian)

Nói rằng bạn có một kịch bản được gọi doEveryDay.sh. Cách không gọi lại để viết kịch bản là:

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

Cách gọi lại để viết nó chỉ đơn giản là:

#! /bin/bash
doSomething

Sau đó, trong crontab, bạn sẽ thiết lập một cái gì đó như

0 0 * * *     doEveryDay.sh

Sau đó, bạn sẽ không cần phải viết mã để chờ sự kiện kích hoạt mà thay vào đó dựa vào cronđể gọi lại mã của bạn.


Bây giờ, hãy xem xét CÁCH BẠN sẽ viết mã này trong bash.

Làm thế nào bạn sẽ thực thi một tập lệnh / hàm khác trong bash?

Hãy viết một hà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
}

Bây giờ bạn đã tạo một chức năng chấp nhận gọi lại. Bạn chỉ có thể gọi nó như thế này:

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

Tất nhiên, chức năng cứ sau 24 giờ không bao giờ trở lại. Bash là một chút độc đáo ở chỗ chúng ta có thể dễ dàng làm cho nó không đồng bộ và sinh ra một quy trình bằng cách nối thêm &:

every24hours 'curl google.com' &

Nếu bạn không muốn đây là một chức năng, bạn có thể thực hiện điều này như một tập lệnh thay thế:

#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

Như bạn có thể thấy, các cuộc gọi lại trong bash là chuyện nhỏ. Nó chỉ đơn giản là:

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

Và gọi lại gọi đơn giản là:

$SOME_CALLBACK_FUNCTION_OR_SCRIPT

Như bạn có thể thấy biểu mẫu ở trên, các cuộc gọi lại hiếm khi trực tiếp là các tính năng của ngôn ngữ. Họ thường lập trình một cách sáng tạo bằng cách sử dụng các tính năng ngôn ngữ hiện có. Bất kỳ ngôn ngữ nào có thể lưu trữ một con trỏ / tham chiếu / bản sao của một số khối mã / hàm / tập lệnh đều có thể thực hiện các cuộc gọi lại.


Các ví dụ khác về chương trình / tập lệnh chấp nhận cuộc gọi lại bao gồm watchfind(khi được sử dụng với -exectham số)
slebetman

0

Gọi lại là một chức năng được gọi khi một số sự kiện xảy ra. Với bash, cơ chế xử lý sự kiện duy nhất tại chỗ có liên quan đến tín hiệu, thoát vỏ và mở rộng đến các sự kiện lỗi shell, các sự kiện gỡ lỗi và các kịch bản hàm / hàm có nguồn gốc trả về các sự kiện.

Dưới đây là một ví dụ về bẫy tín hiệu gọi lại vô dụng nhưng đơn giản.

Đầu tiên tạo tập lệnh thực hiện cuộc gọi lại:

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

Sau đó chạy tập lệnh trong một thiết bị đầu cuối:

$ ./callback-example

và trên một số khác, gửi USR1tín hiệu cho quá trình shell.

$ pkill -USR1 callback-example

Mỗi tín hiệu được gửi sẽ kích hoạt hiển thị các dòng giống như các tín hiệu này trong thiết bị đầu cuối đầu tiên:

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

ksh93, khi shell triển khai nhiều tính năng mà bashsau này được áp dụng, cung cấp cái mà nó gọi là "chức năng kỷ luật". Các hàm này, không khả dụng với bash, được gọi khi một biến shell được sửa đổi hoặc được tham chiếu (tức là đọc). Điều này mở đường cho các ứng dụng hướng sự kiện thú vị hơn.

Ví dụ: tính năng này cho phép các cuộc gọi lại kiểu X11 / Xt / Motif trên các widget đồ họa được triển khai trong phiên bản cũ của các kshtiện ích mở rộng đồ họa được gọi dtksh. Xem hướng dẫn sử dụng dksh .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.