Bash'te boru çıkışı ve yakalama çıkış durumu


420

Bash uzun çalışan komut çalıştırmak istiyor ve her iki yakalama çıkış durum ve vuruşunu çıkışını.

Yani bunu yapıyorum:

command | tee out.txt
ST=$?

Sorun ST değişkeninin teekomutun değil çıkış durumunu yakalamasıdır . Bunu Nasıl Çözebilirim?

Komutun uzun süredir çalıştığını ve çıktıyı daha sonra görüntülemek için bir dosyaya yönlendirdiğini unutmayın.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Eşleşme - hata bulundu" || echo -e "Eşleşme yok - tümü iyi" Dizinin tüm değerlerini bir kerede test eder ve döndürülen boru değerlerinden herhangi biri sıfır değilse bir hata mesajı verir. Bu, borulu bir durumda hataları tespit etmek için oldukça sağlam bir genelleştirilmiş çözümdür.
Brian S. Wilson

Yanıtlar:


519

Dahili bir Bash değişkeni vardır $PIPESTATUS; komutların son ön plan ardışık düzeninde her komutun çıkış durumunu tutan bir dizidir.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Veya diğer kabuklarla (zsh gibi) birlikte çalışan başka bir alternatif, boru arızasını etkinleştirmek olacaktır:

set -o pipefail
...

Gelmez İlk seçenek değil çalışmak zshbiraz dolayı farklı sözdizimi ısırdı.


21
Burada PIPESTATUS AND Pipefail örnekleri ile iyi bir açıklama var: unix.stackexchange.com/a/73180/7453 .
slm

18
Not: $ PIPESTATUS [0], borudaki ilk komutun çıkış durumunu, $ PIPESTATUS [1] ikinci komutun çıkış durumunu vb. Tutar.
simpleuser

18
Tabii ki, bunun Bash'a özgü olduğunu hatırlamamız gerekiyor: Eğer (örneğin) Android cihazımda BusyBox'ın "sh" uygulamasında veya başka bir "sh" kullanarak başka bir gömülü platformda çalıştırmak için bir komut dosyası yazacak olsaydım varyant, bu işe yaramaz.
Asfand Qazi

4
Tırnaksız değişken genişleme endişe eden İçin: çıkış durumu daima imzasız 8 bitlik tam Bash , bu nedenle bu bilgileri aktarmakla gerek yoktur. Bu genellikle çıkış durumunun açıkça 8 bit olarak tanımlandığı Unix altında tutulur ve POSIX'in kendisi tarafından bile imzasız olduğu varsayılır, örneğin mantıksal olumsuzluğunu tanımlarken .
Palec

3
Ayrıca kullanabilirsiniz exit ${PIPESTATUS[0]}.
Chaoran

142

bash kullanmak set -o pipefailyardımcı olur

pipefail: bir boru hattının dönüş değeri, sıfır olmayan bir durumla çıkan son komutun veya sıfır olmayan bir durumdan çıkmadığında sıfırdır.


23
Komut dosyasının tamamının dikey bağlantı ayarını değiştirmek istemiyorsanız, seçeneği yalnızca yerel olarak ayarlayabilirsiniz:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan

7
@Jaan Bu bir alt kabuk çalıştırır. Bundan kaçınmak istiyorsanız set -o pipefail, komutu yapabilir ve sonra komutu yapabilirsiniz ve hemen ardından set +o pipefailseçeneği kaldırmak için yapın.
Linus Arver

2
Not: soru posteri borunun "genel çıkış kodunu" istemez, "komut" dönüş kodunu ister. İle -o pipefailboru halinde görevinden bilemez ama 'komutu' ve 'tee' hem başarısız olursa, o tee 'dan çıkış kodu almak istiyorum.
t0r0X

@LinusArver, başarılı olan bir komut olduğu için çıkış kodunu temizlemez mi?
carlin.scott

126

Aptal çözüm: Adlandırılmış bir borudan (mkfifo) bağlanması. Sonra komut ikinci olarak çalıştırılabilir.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?

20
Bu soruda basit sh Unix kabuğu için de çalışan tek cevap budur . Teşekkürler!
JamesThomasMoon1979

3
@DaveKennedy: "Açık, bash sözdiziminin karmaşık bilgisini gerektirmeyen" gibi aptal
EFraim

10
Bash'ın ekstra yetenekleri avantajına sahip olduğunuzda bash cevapları daha zarif olsa da, bu daha çapraz platform çözümüdür. Ayrıca, uzun süren bir komut yaptığınızda, bir isim borusu genellikle en esnek yol olduğu için genel olarak düşünmeye değer bir şeydir. Bazı sistemlerin doğru olmadığını hatırlamıyorsam mkfifogerekli mknod -polmadığını ve gerektirebileceğini belirtmek gerekir .
Haravikk

3
Bazen yığın taşması sırasında, yüzlerce kez oylayacağınız cevaplar vardır, böylece insanlar anlamsız başka şeyler yapmayı bırakırlar, bu onlardan biri. Teşekkürler bayım.
Dan Chase

1
Birisi ile ilgili bir sorun varsa mkfifoveya mknod -p: benim durumumda boru dosyası oluşturmak için uygun komut oldu mknod FILE_NAME p.
Karol Gil

36

Bir kanaldaki her komutun çıkış durumunu veren bir dizi var.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1

26

Bu çözüm, bash'a özgü özellikler veya geçici dosyalar kullanmadan çalışır. Bonus: Sonunda çıkış durumu aslında bir çıkış durumudur ve dosyadaki bazı dizeler değildir.

Durum:

someprog | filter

Eğer çıkış durumunu istiyorum someprogve çıktı filter.

İşte benim çözümüm:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Ayrıntılı bir açıklama ve alt kabuklar ve bazı uyarılar olmadan bir alternatif için unix.stackexchange.com'da aynı soruya cevabımı görün .


20

Komutu bir alt kabukta birleştirerek PIPESTATUS[0]ve sonucunu uygulayarak, exitilk komutunuzun dönüş değerine doğrudan erişebilirsiniz:

command | tee ; ( exit ${PIPESTATUS[0]} )

İşte bir örnek:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

sana vereceğim:

return value: 1


4
Teşekkürler, bu yapıyı kullanmama izin verdi: VALUE=$(might_fail | piping)PIPESTATUS'u ana kabukta ayarlamadı, ancak hata seviyesini ayarladı. Kullanarak: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})İstediğimi istiyorum.
vaab

@vaab, bu sözdizimi gerçekten hoş görünüyor ama bağlamında 'borulama' ne anlama geldiğinde kafam karıştı? Bu sadece 'tee' veya might_fail'in çıktısında herhangi bir işlem yapacak mı? ty!
AnneTheAgile

1
@AnneTheAgile 'piping' benim örneğimde errlvl'i görmek istemediğiniz komutları ifade eder. Örneğin: 'tee', 'grep', 'sed', ... 'in herhangi biri veya herhangi bir borulu kombinasyonu ... komut: daha sonra ana komutun errlevel'i ile ilgilenirsiniz (örneğimde 'might_fail' olarak adlandırdığım) ama yapım olmadan tüm atama son borulu komutun burada anlamsız olan errlvlini döndürür. Bu daha net mi?
vaab

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}değil tee ama grep filtreleme durumunda
user1742529

12

Bu yüzden lesmana gibi bir cevaba katkıda bulunmak istedim, ama benimkinin belki biraz daha basit ve biraz daha avantajlı saf Bourne kabuğu çözümü olduğunu düşünüyorum:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Bence bu en iyi içten dışa açıklanmıştır - command1 stdout'ta (dosya tanımlayıcı 1) normal çıktısını yürütecek ve yazdıracaktır, daha sonra, printf icommand1'in çıkış kodunu stdout'ta yürütecek ve yazacaktır, ancak bu stdout yönlendirilir dosya tanıtıcı 3.

Komut1 çalışırken, stdout'u command2'ye bağlanır (printf'in çıkışı asla command2'ye gönderilmez çünkü pipo'nun okuduğu 1 yerine dosya tanımlayıcı 3'e göndeririz). Daha sonra command2'nin çıktısını dosya tanımlayıcı 4'e yönlendiririz, böylece dosya tanımlayıcı 1'in dışında kalır - çünkü dosya tanımlayıcı 1'in biraz sonra ücretsiz olmasını istiyoruz, çünkü dosya tanımlayıcı 3'teki printf çıktısını dosya tanımlayıcıya geri getireceğiz 1 - çünkü komut ikamesi (backticks), yakalayacak ve değişkene yerleştirilecek olan budur.

Son sihir biti, önce exec 4>&1ayrı bir komut olarak yaptığımızdır - dosya tanımlayıcı 4'ü dış kabuğun stdout'unun bir kopyası olarak açar. Komut ikamesi, standartta yazılanları içindeki komutların perspektifinden yakalar - ancak komut2'nin çıkışı komut ikamesi söz konusu olduğunda tanımlayıcı 4'e dosyalayacağından, komut ikamesi onu yakalamaz - ancak komut yerine "dışarı" alır hala etkili bir şekilde komut dosyasının genel dosya tanımlayıcı 1 gidiyor.

( exec 4>&1Ayrı bir komut olması gerekir, çünkü birçok ortak kabuk, bir ikame kullanan "harici" komutta açılan bir komut tanımlayıcısının içindeki bir dosya tanımlayıcısına yazmaya çalıştığınızda hoşunuza gitmez. Bunu yapmanın en basit taşınabilir yolu.)

Komutların çıktıları birbirini atlıyormuş gibi ona daha az teknik ve daha eğlenceli bir şekilde bakabilirsiniz: komut1 komutları komut2'ye atlar, ardından printf'in çıkışı komut 2'nin üzerine atlar, böylece komut2 yakalamaz ve sonra komut 2'nin çıktısı, tıpkı printf değişkenle yakalanacak şekilde ikame tarafından yakalanmak için tam zamanında geldiği gibi komut ikamesinin üstünden ve dışına atlar ve komut2'nin çıktısı, tıpkı normal bir boruda.

Ayrıca, anladığım kadarıyla, $?borudaki ikinci komutun dönüş kodunu hala içerecektir, çünkü değişken atamaları, komut ikameleri ve bileşik komutların hepsi, içindeki komutun dönüş koduna etkili bir şekilde şeffaftır, bu nedenle command2 yayılmalı - bu ve ek bir işlev tanımlamak zorunda değilim, bu yüzden lesmana tarafından önerilenden biraz daha iyi bir çözüm olabileceğini düşünüyorum.

Uyarılar lesmana'dan bahsetmişken, command1'in bir noktada 3 veya 4 dosya tanımlayıcılarını kullanması mümkündür, böylece daha sağlam olmak için şunları yaparsınız:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Örneğimde bileşik komutları kullandığımı, ancak alt kabukların ( ( )bunun yerine kullanmak { }da işe yarayacağını, ancak belki de daha az verimli olabileceğini) unutmayın.

Komutlar, dosya tanımlayıcılarını, bunları başlatan işlemden devralır, böylece ikinci satırın tamamı dosya tanımlayıcı dörtünü devralır ve ardından bileşik komutu 3>&1dosya tanımlayıcı üçünü devralır. Böylece, 4>&-iç bileşik komutunun dosya tanımlayıcı dörtünü 3>&-devralmayacağından ve dosya tanımlayıcı üçünü devralmayacağından, komut1 'daha temiz', daha standart bir ortam alır. İçini de 4>&-yanına taşıyabilirsiniz 3>&-, ama neden sadece kapsamını mümkün olduğunca sınırlamakla kalmayacağını anlıyorum.

Ne sıklıkta şeyler dosya tanımlayıcı üç ve dört doğrudan kullandığınızdan emin değilim - çoğu zaman programların şu anda kullanılmayan dosya tanımlayıcıları döndüren syscalls kullandığını düşünüyorum, ancak bazen kod doğrudan dosya tanımlayıcı 3'e yazar, ben tahmin (Ben bir program açık olup olmadığını görmek için bir dosya tanımlayıcı kontrol ve açıksa onu kullanarak, ya da değilse farklı davranır hayal). Bu nedenle, ikincisi akılda tutmak ve genel amaçlı vakalar için kullanmak en iyisidir.


Güzel açıklama!
selurvedu

6

Ubuntu ve Debian'da bunu yapabilirsiniz apt-get install moreutils. Bu mispipe, borudaki ilk komutun çıkış durumunu döndüren adlı bir yardımcı program içerir .


5
(command | tee out.txt; exit ${PIPESTATUS[0]})

@ CODAR'ın cevabından farklı olarak, bu ilk komutun orijinal çıkış kodunu döndürür, sadece başarı için 0 ve başarısızlık için 127 değil. Ama @Chaoran'ın işaret ettiği gibi, sadece arayabilirsiniz ${PIPESTATUS[0]}. Ancak her şeyin parantez içine alınması önemlidir.


4

Bash dışında şunları yapabilirsiniz:

bash -o pipefail  -c "command1 | tee output"

Bu, örneğin kabuğun olması beklenen ninja komut dosyalarında kullanışlıdır /bin/sh.


3

PIPESTATUS [@], boru komutu döndükten hemen sonra bir diziye kopyalanmalıdır. PIPESTATUS [@] 'da yapılan herhangi bir okuma içeriği siler. Tüm boru komutlarının durumunu kontrol etmeyi planlıyorsanız, başka bir diziye kopyalayın. "$?" "$ {PIPESTATUS [@]}" öğesinin son öğesiyle aynı değerdir ve okumak "$ {PIPESTATUS [@]}" yı yok ediyor gibi görünüyor, ancak bunu kesinlikle doğrulamamıştım.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Boru bir alt kabukta ise bu çalışmaz. Bu soruna bir çözüm için
bkz. Backticked komutundaki bash pipetatus?


3

Bunu düz bash'da yapmanın en basit yolu , bir boru hattı yerine süreç ikamesini kullanmaktır. Birkaç fark vardır, ancak muhtemelen kullanım durumunuz için çok fazla önemli değildir:

  • Bir boru hattı çalıştırırken, bash tüm işlemler tamamlanana kadar bekler.
  • Ctrl-C'yi bash'a göndermek, sadece ana olanı değil, bir boru hattının tüm süreçlerini öldürmesini sağlar.
  • pipefailOpsiyon ve PIPESTATUSdeğişken süreç ikamesi alakasız.
  • Muhtemelen daha fazla

Süreç ikamesi ile, bash sadece süreci başlatır ve unutur, içinde bile görünmez jobs.

Bahsedilen farklar bir yana consumer < <(producer)ve producer | consumeresasen eşdeğerdir.

Hangisinin "ana" işlem olduğunu çevirmek istiyorsanız, sadece komutları ve ikame yönünü çevirmeniz yeterlidir producer > >(consumer). Senin durumunda:

command > >(tee out.txt)

Misal:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

Dediğim gibi, boru ifadesinden farklılıklar var. Boru kapanmasına duyarlı olmadığı sürece işlem asla durmayabilir. Özellikle, stdout'unuza bir şeyler yazmaya devam edebilir, bu da kafa karıştırıcı olabilir.


1

Saf kabuk çözeltisi:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

Ve şimdi ikincisi ile catdeğiştirildi false:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

İlk kedinin de başarısız olduğunu lütfen unutmayın, çünkü stdout üzerinde kapanır. Günlükteki başarısız komutların sırası bu örnekte doğrudur, ancak buna güvenmeyin.

Bu yöntem, ayrı komutlar için stdout ve stderr yakalanmasına izin verir, böylece bir hata oluşursa bunu bir günlük dosyasına da dökebilir veya herhangi bir hata yoksa (dd çıktısı gibi) silebilirsiniz.


1

@ Brian-s-wilson'ın cevabına dayanır; bu bash yardımcı işlevi:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

böylece kullanılır:

1: get_bad_things başarılı olmalı, ancak çıktı üretmemelidir; ama ürettiği çıktıyı görmek istiyoruz

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: tüm boru hattı başarılı olmalı

thing | something -q | thingy
pipeinfo || return

1

Bazen bash'ın ayrıntılarını incelemek yerine harici bir komut kullanmak daha basit ve daha açık olabilir. boru hattı , minimum işlem komut dosyası dili yürütme işleminden , tıpkı bir shboru hattının yaptığı gibi ikinci komutun * dönüş koduyla çıkar , ancak aksine sh, borunun yönünün tersine çevrilmesine izin verir, böylece üreticinin dönüş kodunu yakalayabiliriz süreç (aşağıdakilerin tümü shkomut satırındadır, ancak execlineyüklüdür):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

Kullanmanın pipelineyanıtı # 43972501 cevap kullanılan bash işlem ikamesi ile yerel bash boru hatları aynı farklılıklar vardır .

* Aslında pipelinebir hata olmadığı sürece hiç çıkmaz. İkinci komutta çalışır, bu yüzden geri dönen ikinci komuttur.

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.