2009-08-03 106 views
313

我想在Bash中執行長時間運行的命令,並且都捕獲它的退出狀態,並且它的輸出爲teeBash中的管道輸出和捕獲退出狀態

所以我這樣做:

command | tee out.txt 
ST=$? 

的問題是,可變ST捕獲的tee退出狀態,而不是命令。我該如何解決這個問題?

請注意,命令長時間運行並將輸出重定向到文件以便稍後查看對我來說不是一個好的解決方案。

+1

[[「$ {PIPESTATUS [@]}」=〜[^ 0 \]]] && echo -e「Match - error found」|| echo -e「不匹配 - 全部好」 這將一次測試數組的所有值,並在返回的任何管道值不爲零時給出錯誤消息。這是一個非常強大的通用解決方案,用於檢測管道狀況中的錯誤。 – 2015-03-31 19:08:47

+0

http://unix.stackexchange.com/questions/14270/get-exit-status-of-process-thats-piped-to-another – 2016-11-15 17:22:58

回答

408

有稱爲$PIPESTATUS內部擊變量;它是一個數組,用於保存命令的最後一個前臺管道中每個命令的退出狀態。

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

或者另一種選擇,其也可以與其他shell(如zsh中)將是使pipefail:

set -o pipefail 
... 

第一個選項確實zsh工作,由於一點點不同的語法。

87

啞解決方案:通過命名管道(mkfifo)連接它們。然後該命令可以運行第二個。

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

這是這個問題的唯一答案,也適用於簡單的** sh ** Unix shell。謝謝! – JamesThomasMoon1979 2014-09-30 04:08:13

+0

爲什麼這是愚蠢的? – 2016-03-02 02:14:15

+1

@DaveKennedy:愚蠢如「顯而易見,不需要複雜的bash語法知識」 – EFraim 2016-03-02 18:18:37

33

有一個數組可以爲管道中的每個命令提供退出狀態。

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

此解決方案不使用bash特定功能或臨時文件。獎金:最後退出狀態實際上是退出狀態,而不是文件中的某個字符串。

現狀:

someprog | filter 

要從someprog退出狀態,並從filter輸出。

這裏是我的解決方案:

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

echo $? 

對於如何工作的詳細解釋和一些注意事項見my answer for the same question on unix.stackexchange.com

16

通過結合PIPESTATUS[0]並在子shell執行exit命令的結果,您可以直接訪問您的初始命令的返回值:

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

下面是一個例子:

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

會給你:

return value: 1

3

PIPESTATUS [@]必須在管道命令返回後立即複製到數組。 任何讀取PIPESTATUS [@]將擦除內容。 如果您打算檢查所有管道命令的狀態,請將其複製到另一個陣列。 「$?」是與「$ {PIPESTATUS [@]}」的最後一個元素相同的值, 並閱讀它似乎破壞了「$ {PIPESTATUS [@]}」,但我沒有完全驗證這一點。

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

如果管道位於子外殼中,這將不起作用。爲了解決這個問題,
看到使用bash的set -o pipefail是有幫助的bash pipestatus in backticked command?

123

pipefail:管道的返回值是 最後命令的狀態退出與非零狀態, 或零,如果沒有命令用非零狀態

3

在Ubuntu和Debian退出,你可以apt-get install moreutils。這包含一個名爲mispipe的實用程序,該實用程序返回管道中第一個命令的退出狀態。

1

純殼溶液:

% 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 

現在與第二cat通過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 

請注意第一貓也將失敗,因爲它的標準輸出得到它關閉。在本例中,日誌中失敗命令的順序是正確的,但不要依賴它。

該方法允許捕獲單個命令的stdout和stderr,因此如果發生錯誤,您也可以將其轉儲到日誌文件中,或者如果沒有錯誤(如dd的輸出),則刪除它。

7

,所以我想貢獻像lesmana的答案,但我認爲我也許是比較簡單一點,稍微有利純的Bourne外殼的解決方案:

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

我覺得這是最好的從裏面解釋出 - 命令1將執行並打印在標準輸出上的常規輸出(文件描述符1),那麼一旦它的完成,printf的將其標準輸出執行並打印icommand1的退出代碼,但標準輸出重定向到文件描述符3

雖然command1正在運行,它的stdout被傳送到command2(printf的輸出永遠不會使它成爲command2 beca使用我們發送它到文件描述符3而不是1,這是管道讀取的內容)。然後我們將command2的輸出重定向到文件描述符4,以便它也停留在文件描述符1之外 - 因爲我們稍後需要文件描述符1空閒,因爲我們會將文件描述符3上的printf輸出返回到文件描述符1 - 因爲這就是命令替換(反引號)將捕獲的內容,並且這將被放入變量中。

神奇的最後一點是第一個exec 4>&1作爲一個單獨的命令 - 它打開文件描述符4作爲外殼的標準輸出的副本。命令替換將從其內部命令的角度捕獲任何寫在標準上的內容 - 但是由於命令替換所涉及的命令2的輸出將轉到文件描述符4,所以命令替換不會捕獲它 - 但是一旦它得到「輸出」的命令替換它仍然有效地去腳本的整個文件描述符1.

exec 4>&1必須是一個單獨的命令,因爲許多常見的shell不喜歡它,當你試圖寫一個文件描述符在一個命令替換裏面,這是在使用替換的「外部」命令中打開的,所以這是最簡單的便攜方式)

你可以看看它的技術更少,更好玩辦法,就像這些命令的輸出相互跳躍一樣:command1將命令轉換爲command2,然後printf的輸出會跳過命令2,以使命令2不會捕獲它,然後命令2的輸出會跳過命令替換跳過,就像printf恰好及時地被替換捕獲,以便它在變量中結束,並且command2的輸出以正常的管道寫入標準輸出的方式進行。

另外,據我所知,$?仍將包含所述第二命令的返回代碼在管,因爲變量賦值,命令替換和化合物命令都是有效透明的,以在其內部的命令的返回代碼,所以command2的返回狀態應該被傳播出去 - 而不必定義一個額外的函數,這就是爲什麼我認爲這可能是一個比lesmana提出的更好的解決方案。

每告誡lesmana提到,它可能是命令1將在某個時候最終使用文件描述符3或4,所以要更加強勁,你會怎麼做:

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

請注意,我用的複合命令在我的例子,但(使用()代替{ }也將工作,但也許是效率較低。)子shell

命令繼承從啓動它們的進程文件描述符,所以整個第二線將繼承文件描述符四,和複合命令後跟3>&1將繼承文件描述符三。因此,4>&-確保內層複合命令不會繼承文件描述符四,並且3>&-不會繼承文件描述符三,因此command1會得到一個「更乾淨」,更標準的環境。您也可以移動內部4>&-旁邊3>&-,但我想爲什麼不盡可能限制其範圍。

我不知道事情多頻繁地使用文件描述符三和四直接 - 我認爲大多數時間程序使用系統調用返回暫時不使用的文件描述符,但有時代碼寫入文件描述符3我猜(我可以想象一個程序檢查一個文件描述符,看看它是否打開,如果是的話就使用它,或者如果不是則使用不同的行爲)。所以後者可能最好記住並用於通用的情況。

1

基於@ brian-s-wilson的回答;這個慶典的輔助功能:這樣

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

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

使用:

1:get_bad_things必須成功,但它應該不會產生輸出;但我們要看到輸出,它併產生

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

2:所有管道必須成功

thing | something -q | thingy 
pipeinfo || return 
2

外面的bash,你可以這樣做:

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

這是有用的,例如在忍者腳本中,shell預計爲/bin/sh

1

使用外部命令有時可能會更簡單明瞭,而不是深入探討bash的細節。 pipeline,來自最小進程腳本語言execline,與第二個命令*的返回碼退出,就像sh管道一樣,但與sh不同,它允許反轉管道的方向,以便我們可以捕獲返回代碼生產者進程(下面是所有sh命令行上,但與execline安裝):

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

使用pipeline具有天然的bash管道如答案#43972501使用的bash進程取代相同的差異。

*實際上pipeline根本不會退出,除非出現錯誤。它執行到第二個命令,所以它是第二個返回的命令。

2

在純bash中最簡單的方法是使用process substitution而不是管道。有幾個不同點,但它們可能對您的用例無關緊要:

  • 運行管道時,bash會等待所有進程完成。
  • 將Ctrl-C發送到bash使其殺死管道的所有進程,而不僅僅是主進程。
  • pipefail選項和PIPESTATUS變量與進程替換無關。
  • 可能更

隨着進程替換,慶典纔剛剛起步的過程,並忘掉它,它甚至不是在jobs可見。

撇開差異,consumer < <(producer)producer | consumer基本上是等價的。

如果您想翻轉哪一個是「主」過程,您只需將命令和替換方向翻轉到producer > >(consumer)即可。在你的情況:

command > >(tee out.txt) 

實施例:

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

正如我所說的,有從管表達的差異。除非它對管道關閉敏感,否則該過程可能永遠不會停止運行。特別是,它可能會繼續向標準輸出寫入內容,這可能會令人困惑。

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

與@cODAR的回答不同,它返回第一個命令的原始退出碼,不僅成功爲0,失敗爲127。但正如@Chaoran指出的,你可以撥打${PIPESTATUS[0]}。然而,所有內容都放在括號內是很重要的。