Hành vi đúng của bẫy EXIT và ERR khi sử dụng `set -eu`


27

Tôi đang quan sát một số hành vi kỳ lạ khi sử dụng set -e( errexit), set -u( nounset) cùng với bẫy ERR và EXIT. Chúng có vẻ liên quan, vì vậy đặt chúng vào một câu hỏi có vẻ hợp lý.

1) set -ukhông kích hoạt bẫy ERR

  • Mã số:

    #!/bin/bash
    trap 'echo "ERR (rc: $?)"' ERR
    set -u
    echo ${UNSET_VAR}
  • Dự kiến: Bẫy ERR được gọi, RC! = 0
  • Thực tế: Bẫy ERR không được gọi, RC == 1
  • Lưu ý: set -ekhông thay đổi kết quả

2) Sử dụng set -eumã thoát trong bẫy EXIT là 0 thay vì 1

  • Mã số:

    #!/bin/bash
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
  • Dự kiến: Bẫy EXIT được gọi, RC == 1
  • Thực tế: Bẫy EXIT được gọi, RC == 0
  • Lưu ý: Khi sử dụng set +e, RC == 1. Bẫy EXIT trả về RC thích hợp khi bất kỳ lệnh nào khác đưa ra lỗi.
  • Chỉnh sửa: Có một bài viết SO về chủ đề này với một bình luận thú vị cho thấy rằng điều này có thể liên quan đến phiên bản Bash đang được sử dụng. Thử nghiệm đoạn mã này với Bash 4.3.11 cho kết quả RC = 1, vì vậy điều đó tốt hơn. Thật không may, việc nâng cấp Bash (từ 3.2.51) trên tất cả các máy chủ hiện tại là không thể, vì vậy chúng tôi phải đưa ra một số giải pháp khác.

Bất cứ ai có thể giải thích một trong những hành vi này?

Tìm kiếm các chủ đề này không thành công lắm, điều này khá đáng ngạc nhiên với số lượng bài đăng trên các cài đặt và bẫy của Bash. Có một chủ đề diễn đàn , mặc dù, nhưng kết luận là không hài lòng.


3
Từ 4 trở đi, tôi nghĩ bashđã phá vỡ tiêu chuẩn và bắt đầu đặt bẫy vào các mạng con. Cái bẫy được cho là sẽ được thực hiện trong cùng một môi trường từ khi quay trở lại, nhưng bashđã không thực hiện được điều đó trong một lúc.
mikeerv

1
Đợi một chút - bạn có muốn một giải pháp hoặc một lời giải thích? Và nếu bạn muốn một giải pháp, thì một giải pháp cho chính xác những gì? Bạn muốn điều gì xảy ra? set -eset -ucả hai đều được thiết kế đặc biệt để tiêu diệt một lớp vỏ kịch bản. Sử dụng chúng trong các điều kiện có thể kích hoạt ứng dụng của họ sẽ giết chết một lớp vỏ theo kịch bản. Không có xung quanh đó, ngoại trừ việc không sử dụng chúng, và thay vào đó để kiểm tra các điều kiện đó khi chúng áp dụng theo một chuỗi mã. Vì vậy, về cơ bản, bạn có thể viết mã shell tốt, hoặc bạn có thể sử dụng set -eu.
mikeerv

2
Trên thực tế, tôi đang tìm kiếm cả hai, vì tôi không thể tìm thấy đủ thông tin về lý do tại sao -usẽ không kích hoạt bẫy ERR (đó là lỗi, vì vậy không nên kích hoạt bẫy) hoặc mã lỗi là 0 thay vì 1. cái sau dường như là một lỗi đã được sửa trong phiên bản sau, vì vậy đó là. Nhưng phần đầu tiên khá khó hiểu nếu bạn chưa nhận ra rằng lỗi trong đánh giá hệ vỏ (mở rộng tham số) và lỗi thực tế trong các lệnh dường như là hai điều khác nhau. Đối với giải pháp, như bạn đã đề xuất, hiện tôi đang cố gắng tránh -euvà kiểm tra thủ công khi cần thiết.
dvdksng

1
@vdsng - Tốt. Đó là cách để đi - bạn nên đăng kịch bản của mình khi bạn làm như một câu trả lời và tự thưởng cho mình tiền thưởng. Tôi thực sự không thích những lựa chọn đó - chúng không cho phép xử lý ngoại lệ theo bất kỳ cách an toàn nào.
mikeerv

1
@dvdsng - tuy nhiên, một trong những tùy chọn đó có thể hữu ích, tuy nhiên, nằm trong ngữ cảnh được chia nhỏ. Và do đó, có thể hình dung rằng bất cứ điều gì bạn đang sử dụng chúng trước đây đều có thể được định vị thành một bối cảnh phụ như: (set -u; : $UNSET_VAR)và tương tự. Loại công cụ này cũng có thể tốt - đôi khi bạn có thể bỏ rất nhiều thứ &&: (set -e; mkdir dir; cd dir; touch dirfile), nếu bạn nhận được sự trôi dạt của tôi. Chỉ là những bối cảnh được kiểm soát - khi bạn đặt chúng làm tùy chọn toàn cầu, bạn sẽ mất kiểm soát và bị kiểm soát. Thường có những giải pháp hiệu quả hơn.
mikeerv

Câu trả lời:


15

Từ man bash:

  • set -u
    • Xử lý các biến và tham số không đặt khác với các tham số đặc biệt "@""*"là lỗi khi thực hiện mở rộng tham số. Nếu mở rộng được thử trên một biến hoặc tham số chưa đặt, shell sẽ in một thông báo lỗi và, nếu không -ihoạt động, sẽ thoát với trạng thái khác.

POSIX tuyên bố rằng, trong trường hợp xảy ra lỗi mở rộng , hệ vỏ không tương tác sẽ thoát khi mở rộng được liên kết với một nội dung đặc biệt của vỏ (đó là một sự phân biệt bashthường xuyên bỏ qua, và vì vậy có thể không liên quan) hoặc bất kỳ tiện ích nào khác bên cạnh .

  • Hậu quả của lỗi Shell :
    • Một lỗi mở rộng là một trong đó xảy ra khi mở rộng vỏ được định nghĩa trong Lời Mở rộng được thực hiện (ví dụ "${x!y}", bởi vì !không phải là một nhà điều hành hợp lệ) ; việc triển khai có thể coi đây là các lỗi cú pháp nếu nó có thể phát hiện ra chúng trong quá trình token hóa, thay vì trong quá trình mở rộng.
    • [A] n shell tương tác sẽ viết một thông báo chẩn đoán lỗi tiêu chuẩn mà không thoát.

Cũng từ man bash:

  • trap ... ERR
    • Nếu một sigspec là ERR , lệnh arg được thực thi bất cứ khi nào một đường ống (có thể bao gồm một lệnh đơn giản) , một danh sách hoặc lệnh ghép trả về trạng thái thoát khác không, theo các điều kiện sau:
      • Các ERR bẫy không được thực hiện nếu lệnh thất bại là một phần của danh sách lệnh ngay lập tức sau một whilehoặc untiltừ khóa ...
      • ... một phần của bài kiểm tra trong một iftuyên bố ...
      • ... một phần của lệnh được thực thi trong một &&hoặc ||danh sách ngoại trừ lệnh theo sau &&hoặc ||...
      • ... bất kỳ lệnh nào trong một đường ống nhưng cuối cùng ...
      • ... hoặc nếu giá trị trả về của lệnh đang được đảo ngược bằng cách sử dụng !.
    • Đây là những điều kiện tương tự được tuân theo bởi tùy chọn errexit -e .

Lưu ý ở trên rằng bẫy ERR là tất cả về việc đánh giá sự trở lại của một số lệnh khác . Nhưng khi xảy ra lỗi mở rộng , không có lệnh chạy để trả về bất cứ thứ gì. Trong ví dụ của bạn, echo không bao giờ xảy ra - bởi vì trong khi shell đánh giá và mở rộng các đối số của nó, nó gặp một -ubiến nset, được chỉ định bởi tùy chọn shell rõ ràng để tạo ra một lối thoát ngay lập tức từ shell, hiện tại.

Và do đó , bẫy EXIT , nếu có, được thực thi và shell thoát với thông báo chẩn đoán và thoát trạng thái khác 0 - chính xác như nó nên làm.

Đối với điều RC: 0 , tôi hy vọng đó là một lỗi cụ thể của phiên bản nào đó - có thể xảy ra với hai trình kích hoạt cho EXIT xảy ra cùng một lúc và một lỗi nhận mã thoát của người khác (không nên xảy ra) . Và dù sao, với một bashnhị phân cập nhật như được cài đặt bởi pacman:

bash <<\IN
    printf "shell options:\t$-\n"
    trap 'echo "EXIT (rc: $?)"' EXIT
    set -eu
    echo ${UNSET_VAR}
IN

Tôi đã thêm dòng đầu tiên để bạn có thể thấy rằng các điều kiện của trình bao là các điều kiện của trình bao theo kịch bản - nó không tương tác. Đầu ra là:

shell options:  hB
bash: line 4: UNSET_VAR: unbound variable
EXIT (rc: 1)

Dưới đây là một số lưu ý liên quan từ các thay đổi gần đây :

  • Đã sửa lỗi khiến các lệnh không đồng bộ không được đặt $?chính xác.
  • Đã sửa lỗi gây ra thông báo lỗi được tạo bởi lỗi mở rộng trong forcác lệnh có số dòng sai.
  • Đã sửa lỗi khiến SIGINTSIGQUIT không thể truy cập được traptrong các lệnh con không đồng bộ.
  • Đã khắc phục sự cố với xử lý ngắt khiến SIGINT thứ hai và tiếp theo bị bỏ qua bởi các vỏ tương tác.
  • Shell không còn chặn nhận tín hiệu trong khi chạy traptrình xử lý cho các tín hiệu đó và cho phép hầu hết các trap trình xử lý được chạy đệ quy (chạy traptrình xử lý trong khi traptrình xử lý đang thực thi) .

Tôi nghĩ đó là cái cuối cùng hoặc cái đầu tiên có liên quan nhất - hoặc có thể là sự kết hợp của cả hai. Một traptrình xử lý về bản chất là không đồng bộ vì toàn bộ công việc của nó là chờ đợi và xử lý các tín hiệu không đồng bộ . Và bạn kích hoạt hai đồng thời với -eu$UNSET_VAR.

Và vì vậy, có lẽ bạn chỉ nên cập nhật, nhưng nếu bạn thích chính mình, bạn sẽ làm điều đó với một lớp vỏ khác hoàn toàn.


Cảm ơn đã giải thích về cách mở rộng tham số được xử lý khác nhau. Điều đó đã làm sáng tỏ rất nhiều điều với tôi.
dvdksng

Tôi đang cấp cho bạn tiền thưởng vì lời giải thích của bạn là hữu ích nhất.
dvdksng

@dvdssng - Gracias. Vì tò mò, bạn đã bao giờ nghĩ ra giải pháp của mình chưa?
mikeerv

9

(Tôi đang sử dụng bash 4.2.53). Đối với phần 1, trang bash man chỉ nói "Thông báo lỗi sẽ được ghi vào lỗi tiêu chuẩn và trình bao không tương tác sẽ thoát". Nó không nói rằng bẫy ERR sẽ được gọi, mặc dù tôi đồng ý rằng nó sẽ hữu ích nếu có.

Để thực dụng, nếu điều bạn thực sự muốn là đối phó sạch hơn với các biến không xác định, một giải pháp khả thi là đặt hầu hết mã của bạn vào trong một hàm, sau đó thực thi hàm đó trong lớp vỏ phụ và phục hồi mã trả về và đầu ra stderr. Đây là một ví dụ trong đó "cmd ()" là hàm:

#!/bin/bash
trap 'rc=$?; echo "ERR at line ${LINENO} (rc: $rc)"; exit $rc' ERR
trap 'rc=$?; echo "EXIT (rc: $rc)"; exit $rc' EXIT
set -u
set -E # export trap to functions

cmd(){
 echo "args=$*"
 echo ${UNSET_VAR}
 echo hello
}
oops(){
 rc=$?
 echo "$@"
 return $rc # provoke ERR trap
}

exec 3>&1 # copy stdin to use in $()
if output=$(cmd "$@" 2>&1 >&3) # collect stderr, not stdout 
then    echo ok
else    oops "fail: $output"
fi

Trên bash của tôi, tôi nhận được

./script my stuff; echo "exit was $?"
args=my stuff
fail: ./script: line 9: UNSET_VAR: unbound variable
ERR at line 15 (rc: 1)
EXIT (rc: 1)
exit was 1

tốt đẹp, một giải pháp thực tế mà thực sự tăng thêm giá trị!
Florian Heigl
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.