Output pipa dan ambil status keluar di Bash

421

Saya ingin menjalankan perintah yang berjalan lama di Bash, dan keduanya menangkap status keluarnya, dan mem - tee outputnya.

Jadi saya melakukan ini:

command | tee out.txt
ST=$?

Masalahnya adalah bahwa variabel ST menangkap status keluar teedan bukan dari perintah. Bagaimana saya bisa memecahkan masalah ini?

Perhatikan bahwa perintah sudah lama berjalan dan mengarahkan output ke file untuk melihatnya nanti bukan solusi yang baik untuk saya.

flybywire
sumber
1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Kecocokan - kesalahan ditemukan" || echo -e "No match - all good" Ini akan menguji semua nilai array sekaligus dan memberikan pesan kesalahan jika salah satu nilai pipa yang dikembalikan tidak nol. Ini adalah solusi umum yang cukup kuat untuk mendeteksi kesalahan dalam situasi pipa.
Brian S. Wilson
unix.stackexchange.com/questions/14270/…
Ciro Santilli 郝海东 冠状 病 六四 六四 事件

Jawaban:

519

Ada variabel Bash internal yang disebut $PIPESTATUS; ini adalah larik yang menyimpan status keluar dari setiap perintah di pipeline foreground perintah terakhir Anda.

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

Atau alternatif lain yang juga bekerja dengan shell lain (seperti zsh) adalah dengan mengaktifkan pipefail:

set -o pipefail
...

Opsi pertama tidak berfungsi zshkarena sintaks yang sedikit berbeda.

codar
sumber
21
Ada penjelasan yang bagus dengan contoh-contoh PIPESTATUS DAN Pipefail di sini: unix.stackexchange.com/a/73180/7453 .
slm
18
Catatan: $ PIPESTATUS [0] memegang status keluar dari perintah pertama dalam pipa, $ PIPESTATUS [1] status keluar dari perintah kedua, dan seterusnya.
simpleuser
18
Tentu saja, kita harus ingat bahwa ini khusus untuk Bash: jika saya (misalnya) menulis skrip untuk dijalankan pada implementasi "sh" BusyBox di perangkat Android saya, atau pada platform tertanam lainnya menggunakan "sh" lainnya varian, ini tidak akan berhasil.
Asfand Qazi
4
Bagi mereka yang khawatir tentang ekspansi variabel yang tidak dikutip: Status keluar selalu bilangan bulat 8-bit unsigned di Bash , oleh karena itu tidak perlu mengutipnya. Ini berlaku di bawah Unix secara umum juga, di mana status keluar didefinisikan sebagai 8-bit secara eksplisit , dan diasumsikan tidak ditandatangani bahkan oleh POSIX sendiri, misalnya ketika mendefinisikan negasi logisnya .
Palec
3
Anda juga bisa menggunakan exit ${PIPESTATUS[0]}.
Chaoran
142

menggunakan bash set -o pipefailsangat membantu

pipefail: nilai kembali dari sebuah pipa adalah status dari perintah terakhir untuk keluar dengan status bukan-nol, atau nol jika tidak ada perintah yang keluar dengan status bukan-nol

Felipe Alvarez
sumber
23
Jika Anda tidak ingin mengubah pengaturan pipefail dari seluruh skrip, Anda dapat mengatur opsi hanya secara lokal:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan
7
@ Jaan Ini akan menjalankan subkulit. Jika Anda ingin menghindari itu, Anda bisa melakukan set -o pipefaildan kemudian melakukan perintah, dan segera setelah itu lakukan set +o pipefailuntuk membatalkan pilihan.
Linus Arver
2
Catatan: poster pertanyaan tidak ingin "kode keluar umum" dari pipa, ia ingin kode pengembalian 'perintah'. Dengan -o pipefaildia akan tahu jika pipa gagal, tetapi jika 'perintah' dan 'tee' gagal, dia akan menerima kode keluar dari 'tee'.
t0r0X
@LinusArver tidak akan menghapus kode keluar karena itu perintah yang berhasil?
carlin.scott
127

Solusi bodoh: Menghubungkan mereka melalui pipa bernama (mkfifo). Maka perintahnya bisa dijalankan kedua.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
EFRAIM
sumber
20
Ini adalah satu-satunya jawaban dalam pertanyaan ini yang juga berfungsi untuk shell Unix sh sederhana . Terima kasih!
JamesThomasMoon1979
3
@DaveKennedy: Bodoh seperti dalam "jelas, tidak memerlukan pengetahuan rumit tentang sintaks bash"
EFraim
10
Sementara jawaban bash lebih elegan ketika Anda memiliki keuntungan dari kemampuan ekstra bash, ini adalah solusi yang lebih lintas platform. Ini juga sesuatu yang layak dipikirkan secara umum karena setiap kali Anda melakukan perintah yang sudah berjalan lama, pipa nama seringkali merupakan cara yang paling fleksibel. Perlu dicatat bahwa beberapa sistem tidak memiliki mkfifodan mungkin meminta mknod -pjika saya ingat benar.
Haravikk
3
Terkadang pada stack overflow ada jawaban yang akan Anda hapus seratus kali sehingga orang akan berhenti melakukan hal-hal lain yang tidak masuk akal, ini salah satunya. Terima kasih Pak.
Dan Chase
1
Jika seseorang memiliki masalah dengan mkfifoatau mknod -p: dalam kasus saya perintah yang tepat untuk membuat file pipa adalah mknod FILE_NAME p.
Karol Gil
36

Ada array yang memberi Anda status keluar dari setiap perintah dalam pipa.

$ 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
Stefano Borini
sumber
26

Solusi ini berfungsi tanpa menggunakan fitur spesifik bash atau file sementara. Bonus: pada akhirnya status keluar sebenarnya adalah status keluar dan bukan string dalam file.

Situasi:

someprog | filter

Anda menginginkan status keluar dari someprogdan keluaran dari filter.

Inilah solusi saya:

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

echo $?

Lihat jawaban saya untuk pertanyaan yang sama di unix.stackexchange.com untuk penjelasan terperinci dan alternatif tanpa subkulit dan beberapa peringatan.

lesmana
sumber
20

Dengan menggabungkan PIPESTATUS[0]dan hasil mengeksekusi exitperintah dalam subkulit, Anda dapat langsung mengakses nilai balik dari perintah awal Anda:

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

Ini sebuah contoh:

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

akan memberimu:

return value: 1

par
sumber
4
Terima kasih, ini memungkinkan saya untuk menggunakan konstruk: VALUE=$(might_fail | piping)yang tidak akan menetapkan PIPESTATUS di shell master tetapi akan mengatur tingkat kesalahannya. Dengan menggunakan: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})Saya mendapatkan keinginan yang saya inginkan.
vaab
@vaab, sintaks itu terlihat sangat bagus tapi saya bingung apa arti 'piping' dalam konteks Anda? Apakah itu hanya di mana orang akan melakukan 'tee' atau apa pun yang memproses output dari might_fail? ty!
AnneTheAgile
1
@AnneTheAgile 'piping' dalam contoh saya adalah perintah yang Anda tidak ingin melihat errlvl. Misalnya: salah satu atau kombinasi pipa 'tee', 'grep', 'sed', ... Tidak jarang bahwa perintah perpipaan ini dimaksudkan untuk memformat atau mengekstrak info dari output yang lebih besar atau log output dari main command: Anda kemudian lebih tertarik pada errlevel dari perintah utama (yang saya sebut 'might_fail' dalam contoh saya) tetapi tanpa konstruk saya seluruh tugas mengembalikan errlvl perintah piped terakhir yang di sini tidak berarti. Apakah ini lebih jelas?
vaab
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}dalam kasus bukan tee tetapi grep filtering
user1742529
12

Jadi saya ingin menyumbangkan jawaban seperti lesmana, tetapi saya pikir jawaban saya mungkin sedikit lebih sederhana dan sedikit lebih menguntungkan dari solusi Bourne-shell:

# 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.

Saya pikir ini paling baik dijelaskan dari dalam ke luar - command1 akan menjalankan dan mencetak output regulernya di stdout (file descriptor 1), kemudian setelah selesai, printf akan mengeksekusi dan mencetak kode keluar icommand1 pada stdout-nya, tetapi stdout itu diarahkan ke deskriptor file 3.

Ketika command1 sedang berjalan, stdout-nya sedang dipipakan ke command2 (output printf tidak pernah membuatnya ke command2 karena kita mengirimnya ke file descriptor 3, bukan 1, yang dibaca oleh pipa). Kemudian kita mengarahkan output command2 ke file descriptor 4, sehingga ia juga tetap keluar dari file descriptor 1 - karena kita ingin file descriptor 1 gratis sebentar kemudian, karena kita akan membawa output printf pada file descriptor 3 kembali ke file descriptor 1 - karena itulah substitusi perintah (backticks), akan menangkap dan itulah yang akan ditempatkan ke dalam variabel.

Bit terakhir sihir adalah yang pertama exec 4>&1kita lakukan sebagai perintah terpisah - itu membuka file descriptor 4 sebagai salinan stdout shell eksternal. Substitusi perintah akan menangkap apa pun yang tertulis pada standar keluar dari perspektif perintah di dalamnya - tetapi karena output command2 akan mengajukan deskriptor 4 sejauh menyangkut substitusi perintah, substitusi perintah tidak menangkapnya - namun begitu mendapat "keluar" dari substitusi perintah itu secara efektif masih pergi ke file deskriptor keseluruhan skrip 1.

(Itu exec 4>&1harus menjadi perintah terpisah karena banyak shell umum tidak suka ketika Anda mencoba menulis ke deskriptor file di dalam substitusi perintah, yang dibuka di "eksternal" perintah yang menggunakan substitusi. Jadi ini adalah cara portabel paling sederhana untuk melakukannya.)

Anda dapat melihatnya dengan cara yang kurang teknis dan lebih menyenangkan, seolah-olah output dari perintah saling melompati satu sama lain: command1 pipa ke command2, maka output printf melompati perintah 2 sehingga perintah2 tidak menangkapnya, dan kemudian output perintah 2 melompati dan keluar dari substitusi perintah sama seperti printf mendarat tepat pada waktunya untuk ditangkap oleh substitusi sehingga berakhir di variabel, dan output command2 berjalan dengan cara yang menyenangkan ditulis ke output standar, sama seperti dalam pipa normal.

Juga, seperti yang saya mengerti, $?masih akan berisi kode kembali dari perintah kedua dalam pipa, karena penugasan variabel, penggantian perintah, dan perintah majemuk semuanya secara efektif transparan ke kode pengembalian dari perintah di dalamnya, sehingga status pengembalian dari command2 harus disebarkan - ini, dan tidak harus mendefinisikan fungsi tambahan, itulah mengapa saya pikir ini mungkin solusi yang agak lebih baik daripada yang diusulkan oleh lesmana.

Menurut peringatan lesmana, ada kemungkinan bahwa command1 pada akhirnya akan menggunakan deskriptor file 3 atau 4, jadi agar lebih kuat, Anda akan melakukan:

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

Perhatikan bahwa saya menggunakan perintah majemuk dalam contoh saya, tetapi subkulit (menggunakan ( )alih-alih { }juga akan berfungsi, meskipun mungkin kurang efisien.)

Perintah mewarisi deskriptor file dari proses yang meluncurkannya, sehingga seluruh baris kedua akan mewarisi deskriptor file empat, dan perintah gabungan diikuti oleh 3>&1akan mewarisi deskriptor file tiga. Jadi 4>&-memastikan bahwa perintah inner compound tidak akan mewarisi file descriptor empat, dan 3>&-tidak akan mewarisi file deskriptor tiga, jadi command1 mendapat lingkungan yang lebih bersih dan lebih standar. Anda juga bisa memindahkan bagian dalam 4>&-ke sebelah 3>&-, tapi saya pikir mengapa tidak membatasi ruang lingkupnya sebanyak mungkin.

Saya tidak yakin seberapa sering hal menggunakan deskriptor file tiga dan empat secara langsung - Saya pikir sebagian besar program waktu menggunakan syscalls yang mengembalikan deskriptor file yang tidak digunakan saat ini, tetapi kadang-kadang kode menulis ke deskriptor file 3 secara langsung, saya tebak (saya bisa membayangkan sebuah program memeriksa deskriptor file untuk melihat apakah itu terbuka, dan menggunakannya jika ada, atau berperilaku berbeda sesuai jika tidak). Jadi yang terakhir mungkin terbaik untuk diingat dan digunakan untuk kasus-kasus tujuan umum.

mtraceur
sumber
Penjelasan yang bagus!
selurvedu
6

Di Ubuntu dan Debian, Anda bisa apt-get install moreutils. Ini berisi utilitas yang disebut mispipeyang mengembalikan status keluar dari perintah pertama di dalam pipa.

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

Tidak seperti @ cODAR jawaban ini mengembalikan kode keluar asli dari perintah pertama dan tidak hanya 0 untuk sukses dan 127 untuk kegagalan. Tapi seperti yang ditunjukkan @Chaoran, Anda bisa menelepon ${PIPESTATUS[0]}. Namun penting bahwa semua dimasukkan ke dalam tanda kurung.

jakob-r
sumber
4

Di luar bash, Anda dapat melakukan:

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

Ini berguna misalnya dalam skrip ninja di mana shell diharapkan berada /bin/sh.

Anthony Scemama
sumber
3

PIPESTATUS [@] harus disalin ke array segera setelah perintah pipa kembali. Setiap pembacaan PIPESTATUS [@] akan menghapus konten. Salin ke array lain jika Anda berencana memeriksa status semua perintah pipa. "$?" bernilai sama dengan elemen terakhir dari "$ {PIPESTATUS [@]}", dan membacanya sepertinya menghancurkan "$ {PIPESTATUS [@]}", tapi saya belum benar-benar memverifikasi ini.

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

Ini tidak akan berfungsi jika pipa berada dalam sub-shell. Untuk solusi masalah itu,
lihat bash pipestatus di perintah backticked?

maxdev137
sumber
3

Cara paling sederhana untuk melakukan ini di bash polos adalah dengan menggunakan proses substitusi alih-alih pipa. Ada beberapa perbedaan, tetapi mereka mungkin tidak terlalu penting untuk kasus penggunaan Anda:

  • Saat menjalankan pipa, bash menunggu hingga semua proses selesai.
  • Mengirim Ctrl-C ke bash membuatnya membunuh semua proses pipa, bukan hanya yang utama.
  • The pipefailpilihan danPIPESTATUS variabel tidak relevan dengan substitusi proses.
  • Mungkin lebih

Dengan penggantian proses, bash baru saja memulai proses dan lupa tentangnya, itu bahkan tidak terlihat jobs .

Menyebutkan perbedaan di samping, consumer < <(producer)danproducer | consumer pada dasarnya setara.

Jika Anda ingin membalik yang mana adalah proses "utama", Anda cukup membalik perintah dan arah substitusi ke producer > >(consumer). Dalam kasus Anda:

command > >(tee out.txt)

Contoh:

$ { 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

Seperti yang saya katakan, ada perbedaan dari ekspresi pipa. Proses ini mungkin tidak pernah berhenti berjalan, kecuali jika sensitif terhadap penutupan pipa. Secara khusus, mungkin terus menulis hal-hal untuk stdout Anda, yang mungkin membingungkan.

klak
sumber
1

Solusi shell murni:

% 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

Dan sekarang dengan yang kedua catdigantikan oleh 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

Harap perhatikan bahwa kucing pertama juga gagal, karena kucing itu sudah ditutup. Urutan perintah gagal dalam log sudah benar dalam contoh ini, tetapi jangan bergantung padanya.

Metode ini memungkinkan untuk menangkap stdout dan stderr untuk masing-masing perintah sehingga Anda dapat membuangnya juga ke dalam file log jika terjadi kesalahan, atau hapus saja jika tidak ada kesalahan (seperti output dari dd).

Coroos
sumber
1

Berdasarkan jawaban @ brian-s-wilson; fungsi bash helper ini:

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

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

digunakan demikian:

1: get_bad_things harus berhasil, tetapi seharusnya tidak menghasilkan output; tapi kami ingin melihat output yang dihasilkannya

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

2: semua pipa harus berhasil

thing | something -q | thingy
pipeinfo || return
Sam Liddicott
sumber
1

Terkadang lebih mudah dan jelas untuk menggunakan perintah eksternal, daripada menggali detail bash. pipeline , dari execline bahasa scripting proses minimal , keluar dengan kode kembali dari perintah kedua *, seperti halnya shpipeline, tetapi tidak seperti shitu, itu memungkinkan membalikkan arah pipa, sehingga kita dapat menangkap kode kembali dari produser proses (di bawah ini semua ada di shbaris perintah, tetapi dengan execlinediinstal):

$ # 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

Menggunakan pipelinememiliki perbedaan yang sama dengan pipa bash asli sebagai pengganti proses bash yang digunakan dalam jawaban # 43972501 .

* Sebenarnya pipelinetidak keluar sama sekali kecuali ada kesalahan. Ini dieksekusi ke dalam perintah kedua, jadi itu adalah perintah kedua yang melakukan pengembalian.

klak
sumber