Bir kabuk borusunda hata kodlarını yakalama


97

Şu anda şöyle bir şey yapan bir senaryom var:

./a | ./b | ./c

Eğer a, b veya c'den herhangi biri bir hata koduyla çıkarsa, bir hata mesajı yazdıracak ve kötü çıktıyı ileriye doğru yönlendirmek yerine duracak şekilde değiştirmek istiyorum.

Bunu yapmanın en basit / en temiz yolu ne olabilir?


7
&&|"Sadece önceki komut başarılı olursa boruya devam et" anlamına gelecek gibi bir şey olması gerekiyor . Sanırım |||"önceki komut başarısız olursa boruya devam et" anlamına gelen (ve muhtemelen Bash 4'ler gibi hata mesajını yönlendiren |&) anlamına da sahip olabilirsiniz .
sonraki duyuruya kadar duraklatıldı.

6
Çünkü @DennisWilliamson, siz "boruyu dur" olamaz a, b, ckomutları sırayla ancak paralel olarak çalıştırılmaz. Diğer bir deyişle, veri sırayla akar aiçin c, asıl a, bve ckomutlar, aynı zamanda (yaklaşık olarak) başlatın.
Giacomo

Yanıtlar:


20

Birinci komutun başarılı olduğu bilinene kadar ikinci komutun ilerlemesini gerçekten istemiyorsanız, muhtemelen geçici dosyalar kullanmanız gerekir. Bunun basit versiyonu:

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]

'1> & 2' yönlendirmesi ayrıca '> & 2' kısaltılabilir; ancak, MKS kabuğunun eski bir sürümü, önceki '1' olmadan hata yeniden yönlendirmesini yanlış idare etti, bu nedenle, güvenilirlik için bu belirsiz gösterimi yıllarca kullandım.

Bu, bir şeyi kesintiye uğratırsanız dosyaları sızdırır. Bombaya dayanıklı (az ya da çok) mermi programlama şunları kullanır:

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

İlk tuzak satırı, rm -f $tmp.[12]; exit 11 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE veya 15 SIGTERM sinyallerinden herhangi biri gerçekleştiğinde veya 0 (kabuk herhangi bir nedenle çıktığında) ' komutları çalıştır' diyor . Bir kabuk betiği yazıyorsanız, son tuzağın yalnızca, kabuk çıkış tuzağı olan 0'daki kapanı kaldırması gerekir (işlem yine de sona ermek üzere olduğundan diğer sinyalleri yerinde bırakabilirsiniz).

Orijinal ardışık düzende, 'c'nin' a 'bitmeden önce' b'den veri okuyor olması mümkündür - bu genellikle arzu edilir (örneğin, yapılması gereken çok çekirdekli çalışma sağlar). Eğer 'b' bir 'sıralama' fazıysa, o zaman bu geçerli olmayacaktır - 'b' herhangi bir çıktı üretmeden önce tüm girdisini görmelidir.

Hangi komutların başarısız olduğunu tespit etmek istiyorsanız, şunları kullanabilirsiniz:

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

Bu basit ve simetriktir - 4 parçalı veya N parçalı bir boru hattına genişletmek önemsizdir.

'Set -e' ile basit deneyler yardımcı olmadı.


1
mktempVeya kullanmanızı tavsiye ederim tempfile.
sonraki duyuruya kadar duraklatıldı.

@Dennis: evet, sanırım mktemp veya tmpfile gibi komutlara alışmam gerekiyor; yıllar önce öğrendiğimde kabuk seviyesinde yoktu. Hızlı bir kontrol yapalım. MacOS X'te mktemp buluyorum; Solaris üzerinde mktemp kullanıyorum ama bunun tek sebebi GNU araçlarını kurmuş olmam; mktemp, antika HP-UX'te mevcut görünüyor. Platformlar arasında çalışan ortak bir mktemp çağrısı olup olmadığından emin değilim. POSIX ne mktemp ne de tmpfile'ı standartlaştırmaz. Erişimim olan platformlarda tmpfile bulamadım. Sonuç olarak, taşınabilir kabuk komut dosyalarındaki komutları kullanamayacağım.
Jonathan Leffler

1
Kullanırken trap, kullanıcının her zaman SIGKILLbir işlemi hemen sonlandırmak için bir işleme gönderebileceğini , bu durumda tuzağınızın etkili olmayacağını unutmayın. Aynı şey, sisteminiz elektrik kesintisi yaşadığında da geçerlidir. Geçici dosyalar oluştururken, kullandığınızdan emin olun, mktempçünkü bu, dosyalarınızı bir yere, yeniden başlatmanın ardından (genellikle içine /tmp) temizlendikleri yere koyacaktır .
josch

158

Gelen Bash kullanabilirsiniz set -eve set -o pipefaildosyanızın başında. ./a | ./b | ./cÜç komut dosyasından herhangi biri başarısız olduğunda sonraki bir komut başarısız olur. Dönüş kodu, ilk başarısız komut dosyasının dönüş kodu olacaktır.

pipefailStandart sh'de bulunmadığını unutmayın .


Boru hatasından haberim yoktu, gerçekten kullanışlı.
Phil Jackson

Bunu etkileşimli kabukta değil, bir komut dosyasına koymayı amaçlamaktadır. Davranışınız -e ayarlamak için basitleştirilebilir; yanlış; aynı zamanda beklenen davranış olan kabuk sürecinden çıkar;)
Michel Samia

11
Not: bu, üç komut dosyasının tümünü çalıştırmaya devam edecek ve ilk hatada boruyu durdurmayacaktır.
hyde

1
@ n2liquid-GuilhermeVieira Btw, "farklı varyasyonlar" ile, özellikle birini veya her ikisini set(toplam 4 farklı sürüm için) kaldırın ve bunun sonuncunun çıktısını nasıl etkilediğini görün echo.
hyde

1
@josch Google'dan bash'ta bunun nasıl yapılacağını ararken bu sayfayı buldum ve yine de "Tam olarak aradığım şey bu". Yanıtları destekleyen birçok kişinin benzer bir düşünce sürecinden geçtiğinden ve etiketlerin tanımlarını ve etiketlerini kontrol etmediğinden şüpheleniyorum.
Troy Daniels

44

${PIPESTATUS[]}Diziyi tam çalıştırmadan sonra da kontrol edebilirsiniz , örneğin şunu çalıştırırsanız:

./a | ./b | ./c

Ardından ${PIPESTATUS}, borudaki her komuttan bir dizi hata kodu olacaktır, bu nedenle orta komut başarısız olursa, aşağıdaki echo ${PIPESTATUS[@]}gibi bir şey içerecektir:

0 1 0

ve bunun gibi bir şey komuttan sonra çalışır:

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

borudaki tüm komutların başarılı olup olmadığını kontrol etmenizi sağlar.


11
Bu bir bashish - bu bir bash uzantısıdır ve Posix standardının bir parçası değildir, bu nedenle çizgi ve kül gibi diğer mermiler onu desteklemeyecektir. Bu #!/bin/sh, başlatan komut dosyalarında kullanmaya çalışırsanız sorun yaşayabileceğiniz anlamına gelir , çünkü shbash değilse işe yaramaz. ( #!/bin/bashBunun yerine kullanmayı hatırlayarak kolayca düzeltilebilir .)
David Verildi

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- $PIPESTATUS[@]yalnızca 0 ve boşluk içeriyorsa 0 döndürür (borudaki tüm komutlar başarılı olursa).
MattBianco

@MattBianco Bu benim için en iyi çözüm. Ayrıca && ile de çalışır, örneğin command1 && command2 | command3bunlardan herhangi biri başarısız olursa, çözümünüz sıfırdan farklı bir değer döndürür.
DavidC

8

Ne yazık ki, Johnathan'ın cevabı geçici dosyalar gerektirir ve Michel ve Imron'un cevapları bash gerektirir (bu soru kabuk olarak etiketlenmiş olsa bile). Başkalarının da belirttiği gibi, sonraki işlemler başlamadan önce boruyu iptal etmek mümkün değildir. Tüm süreçler aynı anda başlatılır ve bu nedenle herhangi bir hata bildirilmeden önce tümü çalışır. Ancak sorunun başlığı aynı zamanda hata kodlarını da soruyordu. Bunlar, ilgili işlemlerden herhangi birinin başarısız olup olmadığını anlamak için boru bittikten sonra alınabilir ve araştırılabilir.

İşte sadece son bileşenin hatalarını değil, borudaki tüm hataları yakalayan bir çözüm. Yani bu, bash'ın pipefail'i gibi, tüm hata kodlarını alabilmeniz açısından daha güçlü .

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

Herhangi bir şeyin başarısız olup olmadığını tespit etmek için, echoherhangi bir komutun başarısız olması durumunda standart hata üzerine bir komut yazdırılır. Daha sonra birleşik standart hata çıktısı kaydedilir $resve daha sonra incelenir. Bu aynı zamanda tüm işlemlerin standart hatalarının standart çıktıya yeniden yönlendirilmesinin nedenidir. Ayrıca bu çıktıyı /dev/nullbir şeylerin ters gittiğinin başka bir göstergesi olarak gönderebilir veya bırakabilirsiniz. /dev/nullSon komutun çıktısını herhangi bir yerde depolamaya ihtiyacınız varsa , son yönlendirmeyi bir dosyayla değiştirebilirsiniz .

Bu yapı ile daha oynamak için ve bu gerçekten, yerini ne olması gerektiği yapar kendinizi ikna ./a, ./bve ./cyürütmek altkabuklarda tarafından echo, catve exit. Bunu, bu yapının tüm çıktıları bir işlemden diğerine gerçekten ilettiğini ve hata kodlarının doğru şekilde kaydedilip kaydedilmediğini kontrol etmek için kullanabilirsiniz.

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.