Bắt mã lỗi trong đường ống shell


97

Tôi hiện có một tập lệnh làm một cái gì đó như

./a | ./b | ./c

Tôi muốn sửa đổi nó để nếu có bất kỳ a, b hoặc c nào thoát ra với mã lỗi, tôi sẽ in thông báo lỗi và dừng thay vì chuyển đầu ra xấu.

Cách đơn giản / sạch sẽ nhất để làm như vậy là gì?


7
Thực sự cần phải có một cái gì đó như &&|vậy có nghĩa là "chỉ tiếp tục đường ống nếu lệnh trước đó thành công". Tôi cho rằng bạn cũng có thể có |||, điều đó có nghĩa là "tiếp tục đường ống nếu lệnh trước đó không thành công" (và có thể chuyển thông báo lỗi như Bash 4 |&).
Tạm dừng cho đến khi có thông báo mới.

6
@DennisWilliamson, bạn không thể "ngăn chặn các ống" vì a, b, clệnh này không chạy liên tục nhưng song song. Nói cách khác, dữ liệu chảy theo tuần tự từ ađến c, nhưng thực tế a, bclệnh bắt đầu (khoảng) cùng một lúc.
Giacomo

Câu trả lời:


20

Nếu bạn thực sự không muốn lệnh thứ hai tiếp tục cho đến khi lệnh đầu tiên được biết là thành công, thì bạn có thể cần sử dụng các tệp tạm thời. Phiên bản đơn giản của nó là:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

Chuyển hướng '1> & 2' cũng có thể được viết tắt '> & 2'; tuy nhiên, một phiên bản cũ của MKS shell đã xử lý sai chuyển hướng lỗi mà không có '1' trước đó, vì vậy tôi đã sử dụng ký hiệu rõ ràng đó để đảm bảo độ tin cậy cho các lứa tuổi.

Điều này làm rò rỉ các tệp nếu bạn làm gián đoạn điều gì đó. Lập trình shell chống bom (nhiều hơn hoặc ít hơn) sử dụng:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

Dòng bẫy đầu tiên cho biết 'chạy lệnh' rm -f $tmp.[12]; exit 1khi bất kỳ tín hiệu nào 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE hoặc 15 SIGTERM xảy ra hoặc 0 (khi shell thoát ra vì bất kỳ lý do gì). Nếu bạn đang viết script shell, thì bẫy cuối cùng chỉ cần gỡ bỏ bẫy về 0, đó là bẫy thoát shell (bạn có thể để nguyên các tín hiệu khác vì quá trình này sắp kết thúc).

Trong đường dẫn ban đầu, khả thi khi 'c' đọc dữ liệu từ 'b' trước khi 'a' kết thúc - điều này thường là mong muốn (ví dụ: nó cho phép nhiều lõi làm việc). Nếu 'b' là giai đoạn 'sắp xếp', thì điều này sẽ không áp dụng - 'b' phải xem tất cả đầu vào của nó trước khi nó có thể tạo ra bất kỳ đầu ra nào của nó.

Nếu bạn muốn phát hiện (các) lệnh nào không thành công, bạn có thể sử dụng:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

Điều này đơn giản và đối xứng - việc mở rộng sang đường ống 4 phần hoặc N phần là điều nhỏ.

Thử nghiệm đơn giản với 'set -e' không giúp được gì.


1
Tôi muốn khuyên bạn nên sử dụng mktemphoặc tempfile.
Tạm dừng cho đến khi có thông báo mới.

@Dennis: vâng, tôi cho rằng tôi nên làm quen với các lệnh như mktemp hoặc tmpfile; chúng không tồn tại ở cấp độ vỏ khi tôi biết được điều đó, ồ rất nhiều năm trước. Hãy kiểm tra nhanh. Tôi tìm thấy mktemp trên MacOS X; Tôi có mktemp trên Solaris nhưng chỉ vì tôi đã cài đặt các công cụ GNU; có vẻ như mktemp hiện diện trên HP-UX cổ. Tôi không chắc liệu có cách gọi chung của mktemp hoạt động trên các nền tảng hay không. POSIX không chuẩn hóa mktemp hay tmpfile. Tôi không tìm thấy tmpfile trên các nền tảng mà tôi có quyền truy cập. Do đó, tôi sẽ không thể sử dụng các lệnh trong các tập lệnh shell di động.
Jonathan Leffler

1
Khi sử dụng, traphãy nhớ rằng người dùng luôn có thể gửi SIGKILLtới một quy trình để chấm dứt nó ngay lập tức, trong trường hợp đó, bẫy của bạn sẽ không có hiệu lực. Tương tự khi hệ thống của bạn gặp sự cố mất điện. Khi tạo tệp tạm thời, hãy đảm bảo sử dụng mktempvì điều đó sẽ đặt tệp của bạn ở đâu đó, nơi chúng được dọn dẹp sau khi khởi động lại (thường là vào /tmp).
josch

155

Trong bash bạn có thể sử dụng set -eset -o pipefailở đầu tệp của bạn. Một lệnh tiếp theo ./a | ./b | ./csẽ không thành công khi bất kỳ tập lệnh nào trong ba tập lệnh không thành công. Mã trả về sẽ là mã trả về của tập lệnh bị lỗi đầu tiên.

Lưu ý rằng pipefailkhông có trong sh tiêu chuẩn .


Tôi không biết về pipefail, thực sự tiện dụng.
Phil Jackson

Nó được thiết kế để đưa nó vào một tập lệnh, không phải trong trình bao tương tác. Hành vi của bạn có thể được đơn giản hóa thành set -e; sai; nó cũng thoát ra quá trình vỏ, dự kiến hành vi;)
Michel Samia

11
Lưu ý: điều này sẽ vẫn thực thi cả ba tập lệnh và không dừng đường ống ở lỗi đầu tiên.
hyde

1
@ n2liquid-GuilhermeVieira Btw, bởi "các biến thể khác nhau", tôi muốn nói cụ thể là xóa một hoặc cả hai set(tổng cộng 4 phiên bản khác nhau) và xem điều đó ảnh hưởng như thế nào đến kết quả của phiên bản cuối cùng echo.
hyde

1
@josch Tôi đã tìm thấy trang này khi tìm kiếm cách thực hiện việc này trong bash từ google và mặc dù, "Đây chính xác là những gì tôi đang tìm kiếm". Tôi nghi ngờ rằng nhiều người ủng hộ câu trả lời cũng trải qua một quá trình suy nghĩ tương tự và không kiểm tra các thẻ và định nghĩa của các thẻ.
Troy Daniels

43

Bạn cũng có thể kiểm tra ${PIPESTATUS[]}mảng sau khi thực thi đầy đủ, ví dụ: nếu bạn chạy:

./a | ./b | ./c

Sau đó, ${PIPESTATUS}sẽ là một mảng mã lỗi từ mỗi lệnh trong ống dẫn, vì vậy nếu lệnh giữa không thành công, echo ${PIPESTATUS[@]}sẽ chứa một cái gì đó như:

0 1 0

và một cái gì đó như thế này chạy sau lệnh:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

sẽ cho phép bạn kiểm tra xem tất cả các lệnh trong đường ống đã thành công hay chưa.


11
Đây là bashish --- nó là một phần mở rộng bash và không phải là một phần của tiêu chuẩn Posix, vì vậy các shell khác như gạch ngang và tro sẽ không hỗ trợ nó. Điều này có nghĩa là bạn có thể gặp rắc rối nếu cố gắng sử dụng nó trong các tập lệnh bắt đầu #!/bin/sh, bởi vì nếu shkhông bash, nó sẽ không hoạt động. (Có thể sửa chữa dễ dàng bằng cách nhớ sử dụng #!/bin/bashthay thế.)
David Given

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- trả về 0 nếu $PIPESTATUS[@]chỉ chứa 0 và khoảng trắng (nếu tất cả các lệnh trong ống đều thành công).
MattBianco

@MattBianco Đây là giải pháp tốt nhất cho tôi. Nó cũng hoạt động với && ví dụ: command1 && command2 | command3Nếu bất kỳ lỗi nào trong số đó không thành công, giải pháp của bạn trả về khác 0.
DavidC

8

Thật không may, câu trả lời của Johnathan yêu cầu tệp tạm thời và câu trả lời của Michel và Imron yêu cầu bash (mặc dù câu hỏi này được gắn thẻ shell). Như những người khác đã chỉ ra, không thể hủy bỏ đường ống trước khi các quá trình sau đó được bắt đầu. Tất cả các quy trình được bắt đầu cùng một lúc và do đó tất cả sẽ chạy trước khi bất kỳ lỗi nào có thể được thông báo. Nhưng tiêu đề của câu hỏi cũng hỏi về mã lỗi. Chúng có thể được truy xuất và điều tra sau khi đường ống hoàn thành để tìm ra liệu có bất kỳ quy trình liên quan nào bị lỗi hay không.

Đây là một giải pháp bắt tất cả các lỗi trong đường ống và không chỉ lỗi của thành phần cuối cùng. Vì vậy, điều này giống như cú lội ống nước của bash, chỉ mạnh hơn theo nghĩa là bạn có thể truy xuất tất cả các mã lỗi.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Để phát hiện xem có lỗi gì không, một echolệnh sẽ in ra lỗi chuẩn trong trường hợp bất kỳ lệnh nào không thành công. Sau đó, kết quả đầu ra lỗi tiêu chuẩn kết hợp được lưu vào $resvà điều tra sau. Đây cũng là lý do tại sao lỗi tiêu chuẩn của tất cả các quy trình được chuyển hướng đến đầu ra tiêu chuẩn. Bạn cũng có thể gửi đầu ra đó đến /dev/nullhoặc để nó như một chỉ báo khác cho biết đã xảy ra sự cố. Bạn có thể thay thế chuyển hướng cuối cùng đến /dev/nullbằng một tệp nếu bạn không muốn lưu trữ đầu ra của lệnh cuối cùng ở bất kỳ đâu.

Để chơi nhiều hơn với cấu trúc này và để thuyết phục bản thân rằng điều này thực sự làm những gì nó cần, tôi thay thế ./a, ./b./cbởi subshells mà thực hiện echo, catexit. Bạn có thể sử dụng điều này để kiểm tra xem cấu trúc này có thực sự chuyển tiếp tất cả đầu ra từ quy trình này sang quy trình khác và các mã lỗi có được ghi lại chính xác hay không.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
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.