Menangkap kode kesalahan dalam pipa shell

97

Saat ini saya memiliki skrip yang melakukan sesuatu seperti

./a | ./b | ./c

Saya ingin memodifikasinya sehingga jika salah satu dari a, b, atau c keluar dengan kode kesalahan, saya mencetak pesan kesalahan dan berhenti alih-alih meneruskan keluaran yang buruk.

Apa cara termudah / terbersih untuk melakukannya?

hugomg
sumber
7
Benar-benar perlu ada sesuatu seperti &&|yang berarti "hanya lanjutkan pipa jika perintah sebelumnya berhasil". Saya kira Anda juga bisa memiliki |||yang berarti "lanjutkan pipa jika perintah sebelumnya gagal" (dan mungkin menyalurkan pesan kesalahan seperti Bash 4 |&).
Dijeda sampai pemberitahuan lebih lanjut.
6
@DennisWilliamson, Anda tidak dapat "menghentikan pipa" karena a, b, cperintah tidak dijalankan secara berurutan tetapi secara paralel. Dengan kata lain, data mengalir secara berurutan dari ake c, tetapi perintah aktual a, bdan cdimulai (secara kasar) pada waktu yang sama.
Giacomo

Jawaban:

20

Jika Anda benar-benar tidak ingin perintah kedua dilanjutkan hingga perintah pertama berhasil, Anda mungkin perlu menggunakan file sementara. Versi sederhananya adalah:

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]

Pengalihan '1> & 2' juga dapat disingkat '> & 2'; Namun, versi lama dari shell MKS salah menangani pengalihan kesalahan tanpa '1' sebelumnya jadi saya telah menggunakan notasi yang tidak ambigu untuk keandalan selama berabad-abad.

Ini membocorkan file jika Anda mengganggu sesuatu. Pemrograman shell tahan bom (lebih atau kurang) menggunakan:

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

Baris perangkap pertama mengatakan 'jalankan perintah' rm -f $tmp.[12]; exit 1ketika salah satu sinyal 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE, atau 15 SIGTERM terjadi, atau 0 (ketika shell keluar karena alasan apa pun). Jika Anda menulis skrip shell, perangkap terakhir hanya perlu menghapus perangkap di 0, yang merupakan perangkap keluar shell (Anda dapat membiarkan sinyal lain di tempatnya karena prosesnya akan segera berakhir).

Dalam pipeline asli, 'c' layak untuk membaca data dari 'b' sebelum 'a' selesai - ini biasanya diinginkan (ini memberikan banyak inti pekerjaan yang harus dilakukan, misalnya). Jika 'b' adalah fase 'sort', maka ini tidak akan berlaku - 'b' harus melihat semua inputnya sebelum dapat menghasilkan output apa pun.

Jika Anda ingin mendeteksi perintah mana yang gagal, Anda dapat menggunakan:

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

Ini sederhana dan simetris - sangat mudah untuk diperpanjang ke pipa 4-bagian atau N-bagian.

Eksperimen sederhana dengan 'set -e' tidak membantu.

Jonathan Leffler
sumber
1
Saya akan merekomendasikan menggunakan mktempatau tempfile.
Dijeda sampai pemberitahuan lebih lanjut.
@ Dennis: ya, saya kira saya harus terbiasa dengan perintah seperti mktemp atau tmpfile; mereka tidak ada di tingkat shell ketika saya mempelajarinya, oh bertahun-tahun yang lalu. Mari kita lakukan pemeriksaan cepat. Saya menemukan mktemp di MacOS X; Saya memiliki mktemp di Solaris tetapi hanya karena saya telah menginstal alat GNU; Sepertinya mktemp hadir pada HP-UX antik. Saya tidak yakin apakah ada permintaan umum mktemp yang berfungsi di berbagai platform. POSIX tidak menstandarisasi mktemp maupun tmpfile. Saya tidak menemukan tmpfile pada platform yang saya akses. Akibatnya, saya tidak dapat menggunakan perintah dalam skrip shell portabel.
Jonathan Leffler
1
Saat menggunakan, trapperlu diingat bahwa pengguna selalu dapat mengirim SIGKILLke suatu proses untuk segera menghentikannya, dalam hal ini jebakan Anda tidak akan berpengaruh. Hal yang sama berlaku saat sistem Anda mengalami pemadaman listrik. Saat membuat file sementara, pastikan untuk menggunakan mktempkarena itu akan meletakkan file Anda di suatu tempat, di mana mereka dibersihkan setelah reboot (biasanya ke /tmp).
josch
156

Di bash Anda dapat menggunakan set -edan set -o pipefaildi awal file Anda. Perintah berikutnya ./a | ./b | ./cakan gagal jika salah satu dari tiga skrip gagal. Kode pengembalian akan menjadi kode pengembalian dari skrip pertama yang gagal.

Perhatikan bahwa pipefailtidak tersedia di sh standar .

Michel Samia
sumber
Saya tidak tahu tentang pipefail, sangat berguna.
Phil Jackson
Ini dimaksudkan untuk memasukkannya ke dalam skrip, bukan di shell interaktif. Perilaku Anda dapat disederhanakan menjadi set -e; Salah; itu juga keluar dari proses shell, yang merupakan perilaku yang diharapkan;)
Michel Samia
11
Catatan: ini masih akan mengeksekusi ketiga skrip, dan tidak menghentikan pipa pada kesalahan pertama.
hyde
1
@ n2liquid-GuilhermeVieira Btw, dengan "variasi yang berbeda", maksud saya secara khusus, hapus satu atau kedua set(untuk total 4 versi yang berbeda), dan lihat bagaimana hal itu mempengaruhi keluaran yang terakhir echo.
hyde
1
@josch Saya menemukan halaman ini ketika mencari bagaimana melakukan ini di bash dari google, dan meskipun, "Inilah yang saya cari". Saya menduga banyak orang yang memberi suara positif pada jawaban melalui proses pemikiran yang serupa dan tidak memeriksa tag dan definisi tag.
Troy Daniels
44

Anda juga dapat memeriksa ${PIPESTATUS[]}array setelah eksekusi penuh, misalnya jika Anda menjalankan:

./a | ./b | ./c

Kemudian ${PIPESTATUS}akan menjadi array kode kesalahan dari setiap perintah di pipa, jadi jika perintah tengah gagal, echo ${PIPESTATUS[@]}akan berisi sesuatu seperti:

0 1 0

dan sesuatu seperti ini dijalankan setelah perintah:

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

akan memungkinkan Anda untuk memeriksa bahwa semua perintah di pipa berhasil.

Imron
sumber
11
Ini bashish --- ini adalah ekstensi bash dan bukan bagian dari standar Posix, jadi shell lain seperti dash dan ash tidak akan mendukungnya. Ini berarti Anda dapat mengalami masalah jika Anda mencoba menggunakannya dalam skrip yang dimulai #!/bin/sh, karena jika shbukan bash, itu tidak akan berfungsi. (Mudah diperbaiki dengan mengingat untuk menggunakan #!/bin/bashsebagai gantinya.)
David Diberikan
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- mengembalikan 0 jika $PIPESTATUS[@]hanya berisi 0 dan spasi (jika semua perintah di pipa berhasil).
MattBianco
@MattBianco Ini adalah solusi terbaik untuk saya. Ia juga bekerja dengan && misalnya command1 && command2 | command3Jika salah satu dari mereka gagal solusi Anda mengembalikan bukan nol.
DavidC
8

Sayangnya, jawaban Johnathan membutuhkan file sementara dan jawaban dari Michel dan Imron membutuhkan bash (meskipun pertanyaan ini diberi tag shell). Seperti yang telah ditunjukkan oleh orang lain, tidak mungkin untuk membatalkan pipa sebelum proses selanjutnya dimulai. Semua proses dimulai sekaligus dan semua akan berjalan sebelum kesalahan dapat dikomunikasikan. Tapi judul pertanyaannya juga menanyakan tentang kode kesalahan. Ini dapat diambil dan diselidiki setelah pipa selesai untuk mencari tahu apakah ada proses yang terlibat gagal.

Berikut adalah solusi yang menangkap semua kesalahan dalam pipa dan tidak hanya kesalahan pada komponen terakhir. Jadi ini seperti pipefail bash, hanya lebih kuat dalam artian Anda dapat mengambil semua kode kesalahan.

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

Untuk mendeteksi apakah ada yang gagal, echoperintah mencetak pada kesalahan standar jika ada perintah yang gagal. Kemudian output kesalahan standar gabungan disimpan $resdan diselidiki nanti. Ini juga mengapa kesalahan standar dari semua proses diarahkan ke keluaran standar. Anda juga dapat mengirim keluaran itu ke /dev/nullatau membiarkannya sebagai indikator lain bahwa ada yang tidak beres. Anda dapat mengganti pengalihan terakhir ke /dev/nulldengan file jika Anda tidak perlu menyimpan keluaran dari perintah terakhir di mana saja.

Untuk bermain lebih banyak dengan konstruksi ini dan untuk meyakinkan diri sendiri bahwa ini benar-benar melakukan apa yang seharusnya, saya menggantinya ./a, ./bdan ./cdengan subkulit yang mengeksekusi echo, catdan exit. Anda dapat menggunakan ini untuk memeriksa bahwa konstruksi ini benar-benar meneruskan semua output dari satu proses ke proses lainnya dan kode kesalahan dicatat dengan benar.

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
josch
sumber