Đầu ra ống và trạng thái thoát chụp trong Bash


421

Tôi muốn thực hiện một lệnh dài chạy trong Bash, và cả hai chụp trạng thái thoát của nó, và tee sản lượng của nó.

Vì vậy, tôi làm điều này:

command | tee out.txt
ST=$?

Vấn đề là biến ST nắm bắt trạng thái thoát teevà không phải của lệnh. Làm sao tôi có thể giải quyết việc này?

Lưu ý rằng lệnh đang chạy lâu và chuyển hướng đầu ra sang một tệp để xem nó sau này không phải là một giải pháp tốt cho tôi.


1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Kết hợp - tìm thấy lỗi" || echo -e "Không khớp - tất cả đều tốt" Điều này sẽ kiểm tra tất cả các giá trị của mảng cùng một lúc và đưa ra thông báo lỗi nếu bất kỳ giá trị đường ống nào được trả về không bằng không. Đây là một giải pháp tổng quát khá mạnh mẽ để phát hiện lỗi trong tình huống đường ống.
Brian S. Wilson

Câu trả lời:


519

Có một biến Bash nội bộ được gọi là $PIPESTATUS; đó là một mảng chứa trạng thái thoát của mỗi lệnh trong đường dẫn tiền cảnh cuối cùng của bạn.

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

Hoặc một giải pháp thay thế khác cũng hoạt động với các shell khác (như zsh) sẽ là kích hoạt pipefail:

set -o pipefail
...

Tùy chọn đầu tiên không hoạt động zshdo cú pháp hơi khác một chút.


21
Có một lời giải thích tốt với các ví dụ về PIPESTATUS VÀ Pipefail tại đây: unix.stackexchange.com/a/73180/7453 .
slm

18
Lưu ý: $ PIPESTATUS [0] giữ trạng thái thoát của lệnh đầu tiên trong đường ống, $ PIPESTATUS [1] trạng thái thoát của lệnh thứ hai, v.v.
đơn giản

18
Tất nhiên, chúng ta phải nhớ rằng đây là đặc thù của Bash: nếu tôi (ví dụ) viết một tập lệnh để chạy trên triển khai "sh" của BusyBox trên thiết bị Android của tôi hoặc trên một số nền tảng nhúng khác sử dụng một số "sh" khác biến thể, điều này sẽ không hoạt động.
Asfand Qazi

4
Đối với những người quan tâm về việc mở rộng biến không được trích dẫn: Trạng thái thoát luôn luôn là số nguyên 8 bit không dấu trong Bash , do đó không cần phải trích dẫn nó. Điều này cũng thuộc về Unix nói chung, trong đó trạng thái thoát được xác định rõ ràng là 8 bit và nó được coi là không dấu ngay cả bởi chính POSIX, ví dụ như khi xác định phủ định logic của nó .
Palec

3
Bạn cũng có thể sử dụng exit ${PIPESTATUS[0]}.
Chaoran

142

sử dụng bash set -o pipefaillà hữu ích

Pipefail: giá trị trả về của một đường ống là trạng thái của lệnh cuối cùng để thoát với trạng thái khác không hoặc bằng 0 nếu không có lệnh nào thoát với trạng thái khác không


23
Trong trường hợp bạn không muốn sửa đổi cài đặt đường ống của toàn bộ tập lệnh, bạn chỉ có thể đặt tùy chọn cục bộ:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan

7
@Jaan Điều này sẽ chạy một subshell. Nếu bạn muốn tránh điều đó, bạn có thể thực hiện set -o pipefailvà sau đó thực hiện lệnh và ngay lập tức sau đó thực hiện set +o pipefailđể bỏ đặt tùy chọn.
Linus Arver

2
Lưu ý: người đăng câu hỏi không muốn có "mã thoát chung" của đường ống, anh ta muốn mã trả về của 'lệnh'. Với -o pipefailanh ta sẽ biết nếu đường ống thất bại, nhưng nếu cả 'lệnh' và 'tee' đều thất bại, anh ta sẽ nhận được mã thoát từ 'tee'.
t0r0X

@LinusArver sẽ không xóa mã thoát vì đó là một lệnh thành công?
carlin.scott

127

Giải pháp ngu ngốc: Kết nối chúng thông qua một đường ống có tên (mkfifo). Sau đó, lệnh có thể được chạy thứ hai.

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

20
Đây là câu trả lời duy nhất trong câu hỏi này cũng hoạt động cho shell Unix sh đơn giản . Cảm ơn!
JamesThomasMoon1979

3
@DaveKennedy: Câm như trong "rõ ràng, không đòi hỏi kiến ​​thức phức tạp về cú pháp bash"
EFraim

10
Mặc dù các câu trả lời bash thanh lịch hơn khi bạn có lợi thế về khả năng bổ sung của bash, đây là giải pháp đa nền tảng hơn. Nói chung, đó cũng là điều đáng để suy nghĩ vì bất cứ khi nào bạn thực hiện một lệnh dài, một ống tên thường là cách linh hoạt nhất. Điều đáng chú ý là một số hệ thống không có mkfifovà thay vào đó có thể yêu cầu mknod -pnếu tôi nhớ đúng.
Harastak

3
Đôi khi trên stack overflow có những câu trả lời mà bạn sẽ đưa ra hàng trăm lần để mọi người ngừng làm những việc khác không có ý nghĩa, đây là một trong số đó. Cảm ơn ngài.
Dan Chase

1
Trong trường hợp ai đó có vấn đề với mkfifohoặc mknod -p: trong trường hợp của tôi, lệnh thích hợp để tạo tệp đường ống là mknod FILE_NAME p.
Karol Gil

36

Có một mảng cung cấp cho bạn trạng thái thoát của mỗi lệnh trong một đường ống.

$ 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

Giải pháp này hoạt động mà không sử dụng các tính năng cụ thể của bash hoặc các tệp tạm thời. Phần thưởng: cuối cùng, trạng thái thoát thực sự là trạng thái thoát và không phải là một chuỗi trong tệp.

Tình hình:

someprog | filter

bạn muốn trạng thái thoát khỏi someprogvà đầu ra từ filter.

Đây là giải pháp của tôi:

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

echo $?

Xem câu trả lời của tôi cho cùng một câu hỏi trên unix.stackexchange.com để được giải thích chi tiết và một giải pháp thay thế không có subshells và một số cảnh báo.


20

Bằng cách kết hợp PIPESTATUS[0]và kết quả của việc thực hiện exitlệnh trong một lớp con, bạn có thể truy cập trực tiếp giá trị trả về của lệnh ban đầu:

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

Đây là một ví dụ:

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

sẽ cung cấp cho bạn:

return value: 1


4
Cảm ơn, điều này cho phép tôi sử dụng cấu trúc: VALUE=$(might_fail | piping)sẽ không đặt PIPESTATUS trong trình bao chính nhưng sẽ đặt lỗi của nó. Bằng cách sử dụng: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})Tôi nhận được tôi muốn.
vaab

@vaab, cú pháp đó trông rất hay nhưng tôi bối rối không biết 'đường ống' nghĩa là gì trong ngữ cảnh của bạn? Có phải đó chỉ là nơi người ta sẽ làm 'tee' hoặc bất cứ điều gì xử lý trên đầu ra của might_fail? ty!
AnneTheAgile

1
@AnneTheAgile 'piping' trong ví dụ của tôi là viết tắt của các lệnh mà bạn không muốn thấy errlvl. Ví dụ: một trong hoặc bất kỳ kết hợp đường ống nào của 'tee', 'grep', 'sed', ... Không có gì lạ khi các lệnh đường ống này có nghĩa là định dạng hoặc trích xuất thông tin từ đầu ra lớn hơn hoặc đầu ra nhật ký của chính lệnh: sau đó bạn quan tâm nhiều hơn đến errlevel của lệnh chính (lệnh mà tôi đã gọi là 'might_fail' trong ví dụ của tôi) nhưng không có cấu trúc của tôi, toàn bộ phép gán sẽ trả về errlvl của lệnh đường ống cuối cùng ở đây là vô nghĩa. Điều này rõ ràng hơn?
vaab

command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}trong trường hợp không phải tee nhưng lọc grep
user1742529

12

Vì vậy, tôi muốn đóng góp một câu trả lời như của lesmana, nhưng tôi nghĩ rằng giải pháp của tôi có lẽ đơn giản hơn một chút và có lợi hơn một chút:

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

Tôi nghĩ rằng điều này được giải thích tốt nhất từ ​​trong ra ngoài - lệnh1 sẽ thực thi và in đầu ra thông thường của nó trên thiết bị xuất chuẩn (mô tả tệp 1), sau đó khi hoàn tất, printf sẽ thực thi và in mã thoát của icommand1 trên thiết bị xuất chuẩn của nó, nhưng thiết bị xuất chuẩn đó được chuyển hướng đến mô tả tập tin 3.

Trong khi lệnh1 đang chạy, thiết bị xuất chuẩn của nó đang được chuyển sang lệnh2 (đầu ra của printf không bao giờ chuyển sang lệnh2 vì chúng tôi gửi nó tới tệp mô tả 3 thay vì 1, đó là những gì đường ống đọc). Sau đó, chúng tôi chuyển hướng đầu ra của lệnh2 sang bộ mô tả tệp 4, do đó nó cũng nằm ngoài bộ mô tả tệp 1 - vì chúng tôi muốn mô tả tệp 1 miễn phí sau một chút, vì chúng tôi sẽ đưa đầu ra printf trên bộ mô tả tệp 3 trở lại vào bộ mô tả tệp 1 - bởi vì đó là những gì thay thế lệnh (backticks), sẽ nắm bắt và đó là những gì sẽ được đặt vào biến.

Điều kỳ diệu cuối cùng là lần đầu tiên exec 4>&1chúng tôi thực hiện như một lệnh riêng - nó mở mô tả tệp 4 dưới dạng bản sao của thiết bị xuất chuẩn của lớp vỏ ngoài. Thay thế lệnh sẽ nắm bắt bất cứ điều gì được viết trên tiêu chuẩn từ phối cảnh của các lệnh bên trong nó - nhưng vì đầu ra của lệnh2 sẽ chuyển sang mô tả 4 khi có liên quan đến việc thay thế lệnh, nên thay thế lệnh sẽ không nắm bắt được - tuy nhiên một khi nó được "ra" thay thế lệnh, nó vẫn thực sự đi đến phần mô tả tập tin tổng thể của tập lệnh 1.

( exec 4>&1Phải là một lệnh riêng vì nhiều shell thông thường không thích nó khi bạn cố ghi vào một mô tả tệp bên trong thay thế lệnh, được mở trong lệnh "bên ngoài" đang sử dụng thay thế. Vì vậy, đây là cách di động đơn giản nhất để làm điều đó.)

Bạn có thể xem xét nó theo cách ít kỹ thuật và vui tươi hơn, vì nếu các đầu ra của các lệnh đang nhảy vọt vào nhau: Command1 pipe sang lệnh2, thì đầu ra của printf nhảy qua lệnh 2 để lệnh2 không bắt được nó, và sau đó Đầu ra của lệnh 2 nhảy qua và thay thế lệnh giống như printf vừa kịp để bị bắt bởi sự thay thế để nó kết thúc trong biến và đầu ra của lệnh2 tiếp tục được ghi vào đầu ra tiêu chuẩn, giống như trong một đường ống bình thường.

Ngoài ra, theo tôi hiểu, nó $?vẫn sẽ chứa mã trả về của lệnh thứ hai trong đường ống, bởi vì các phép gán biến, thay thế lệnh và các lệnh ghép đều hoàn toàn trong suốt đối với mã trả về của lệnh bên trong chúng, do đó trạng thái trả về của Command2 nên được truyền bá - điều này, và không phải xác định một chức năng bổ sung, là lý do tại sao tôi nghĩ rằng đây có thể là một giải pháp tốt hơn so với giải pháp được đề xuất bởi lesmana.

Theo đề xuất của lesmana, có thể lệnh1 sẽ kết thúc bằng cách sử dụng mô tả tệp 3 hoặc 4, vì vậy để mạnh mẽ hơn, bạn sẽ làm:

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

Lưu ý rằng tôi sử dụng các lệnh ghép trong ví dụ của mình, nhưng các lớp con (sử dụng ( )thay vì { }cũng sẽ hoạt động, mặc dù có lẽ sẽ kém hiệu quả hơn.)

Các lệnh kế thừa bộ mô tả tệp từ quá trình khởi chạy chúng, vì vậy toàn bộ dòng thứ hai sẽ kế thừa bộ mô tả tệp bốn và lệnh ghép tiếp theo 3>&1sẽ kế thừa bộ mô tả tệp ba. Vì vậy, 4>&-đảm bảo rằng lệnh ghép bên trong sẽ không kế thừa bộ mô tả tệp bốn và 3>&-sẽ không kế thừa bộ mô tả tệp ba, vì vậy lệnh1 có được 'môi trường sạch hơn', chuẩn hơn. Bạn cũng có thể di chuyển bên trong bên 4>&-cạnh 3>&-, nhưng tôi hiểu tại sao không chỉ giới hạn phạm vi của nó càng nhiều càng tốt.

Tôi không chắc chắn tần suất mọi thứ sử dụng bộ mô tả tệp ba và bốn trực tiếp - Tôi nghĩ rằng hầu hết các chương trình sử dụng các tòa nhà chọc trời trả lại các mô tả tệp không được sử dụng tại thời điểm đó, nhưng đôi khi mã được ghi trực tiếp vào bộ mô tả tệp 3, tôi đoán (tôi có thể tưởng tượng một chương trình kiểm tra một bộ mô tả tệp để xem nó có mở không và sử dụng nó nếu có, hoặc hành xử khác đi nếu không). Vì vậy, cái sau có lẽ là tốt nhất để ghi nhớ và sử dụng cho các trường hợp mục đích chung.


Giải thích tốt đẹp!
selurvedu

6

Trong Ubuntu và Debian, bạn có thể apt-get install moreutils. Điều này chứa một tiện ích được gọi là mispipetrả về trạng thái thoát của lệnh đầu tiên trong đường ống.


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

Không giống như câu trả lời của @ cODAR, điều này trả về mã thoát ban đầu của lệnh đầu tiên và không chỉ 0 cho thành công và 127 cho thất bại. Nhưng như @Chaoran chỉ ra bạn chỉ có thể gọi ${PIPESTATUS[0]}. Tuy nhiên, điều quan trọng là tất cả được đặt trong ngoặc.


4

Ngoài bash, bạn có thể làm:

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

Điều này rất hữu ích, ví dụ trong các kịch bản ninja nơi vỏ được dự kiến /bin/sh.


3

PIPESTATUS [@] phải được sao chép vào một mảng ngay sau khi lệnh pipe trở lại. Mọi lần đọc PIPESTATUS [@] sẽ xóa nội dung. Sao chép nó vào một mảng khác nếu bạn có kế hoạch kiểm tra trạng thái của tất cả các lệnh ống. "$?" là giá trị tương tự như phần tử cuối cùng của "$ {PIPESTATUS [@]}" và đọc nó dường như phá hủy "$ {PIPESTATUS [@]}", nhưng tôi chưa hoàn toàn xác minh điều này.

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

Điều này sẽ không hoạt động nếu đường ống nằm trong vỏ phụ. Để biết giải pháp cho vấn đề đó,
hãy xem bash pipestatus trong lệnh backticky?


3

Cách đơn giản nhất để làm điều này trong bash đơn giản là sử dụng thay thế quy trình vì đường ống dẫn. Có một số khác biệt, nhưng có lẽ chúng không quan trọng lắm đối với trường hợp sử dụng của bạn:

  • Khi chạy một đường ống, bash đợi cho đến khi tất cả các quy trình hoàn tất.
  • Gửi Ctrl-C để bash làm cho nó giết tất cả các quy trình của một đường ống, không chỉ là quy trình chính.
  • Các pipefailtùy chọn vàPIPESTATUS biến không liên quan đến thay thế tiến trình.
  • Có thể nhiều hơn

Với sự thay thế quy trình, bash chỉ bắt đầu quá trình và quên nó, nó thậm chí không hiển thị trong jobs .

Đề cập đến sự khác biệt sang một bên, consumer < <(producer)producer | consumer về cơ bản là tương đương.

Nếu bạn muốn lật cái nào là quá trình "chính", bạn chỉ cần lật các lệnh và hướng thay thế producer > >(consumer). Trong trường hợp của bạn:

command > >(tee out.txt)

Thí dụ:

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

Như tôi đã nói, có sự khác biệt từ biểu thức đường ống. Quá trình có thể không bao giờ ngừng chạy, trừ khi nó nhạy cảm với việc đóng ống. Cụ thể, nó có thể tiếp tục viết những thứ vào thiết bị xuất chuẩn của bạn, điều này có thể gây nhầm lẫn.


1

Dung dịch vỏ nguyên chất:

% 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

Và bây giờ với cái thứ hai được catthay thế bởi 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

Xin lưu ý rằng con mèo đầu tiên cũng thất bại, bởi vì thiết bị xuất chuẩn bị đóng trên nó. Thứ tự của các lệnh thất bại trong nhật ký là chính xác trong ví dụ này, nhưng đừng dựa vào nó.

Phương pháp này cho phép chụp stdout và stderr cho các lệnh riêng lẻ để bạn có thể kết xuất nó vào tệp nhật ký nếu xảy ra lỗi hoặc chỉ xóa nó nếu không có lỗi (như đầu ra của dd).


1

Dựa trên câu trả lời của @ brian-s-wilson; chức năng trợ giúp bash này:

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

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

được sử dụng như vậy:

1: get_bad_things phải thành công, nhưng nó sẽ không tạo ra đầu ra; nhưng chúng tôi muốn thấy đầu ra mà nó tạo ra

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

2: tất cả các đường ống phải thành công

thing | something -q | thingy
pipeinfo || return

1

Đôi khi có thể đơn giản và rõ ràng hơn khi sử dụng lệnh bên ngoài, thay vì đào sâu vào các chi tiết của bash. đường ống , từ quá trình ngôn ngữ kịch bản tối thiểu execline , thoát với mã trở lại của lệnh * thứ hai, giống như một shđường ống có, nhưng không giống như sh, nó cho phép đảo chiều của đường ống, do đó chúng ta có thể nắm bắt được mã trở lại của nhà sản xuất process (bên dưới là tất cả trên shdòng lệnh, nhưng execlineđã cài đặt):

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

Việc sử dụng pipelinecó sự khác biệt tương tự với các đường ống bash riêng như sự thay thế quá trình bash được sử dụng trong câu trả lời # 43942501 .

* Trên thực tế pipelinekhông thoát ra trừ khi có lỗi. Nó thực thi lệnh thứ hai, vì vậy đây là lệnh thứ hai thực hiện trả về.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.