Tại sao set -e không hoạt động bên trong các ô con với dấu ngoặc đơn () theo sau là danh sách OR | |?


30

Gần đây tôi đã chạy một số kịch bản như thế này:

( set -e ; do-stuff; do-more-stuff; ) || echo failed

Điều này có vẻ tốt với tôi, nhưng nó không hoạt động! Các set -ekhông áp dụng, khi bạn thêm ||. Không có điều đó, nó hoạt động tốt:

$ ( set -e; false; echo passed; ); echo $?
1

Tuy nhiên, nếu tôi thêm ||, set -ethì bị bỏ qua:

$ ( set -e; false; echo passed; ) || echo failed
passed

Sử dụng một vỏ thực sự, riêng biệt hoạt động như mong đợi:

$ sh -c 'set -e; false; echo passed;' || echo failed
failed

Tôi đã thử điều này trong nhiều shell khác nhau (bash, dash, ksh93) và tất cả đều hoạt động theo cùng một cách, vì vậy đó không phải là một lỗi. Ai đó có thể giải thích điều này?


Cấu trúc `(....)` `khởi động một shell riêng để chạy nội dung của nó, mọi cài đặt trong nó không áp dụng bên ngoài.
vonbrand

@vonbrand, bạn đã bỏ lỡ điểm. Anh ta muốn nó áp dụng bên trong subshell, nhưng ||bên ngoài subshell ảnh hưởng đến hành vi bên trong subshell.
cjm

1
So sánh (set -e; echo 1; false; echo 2)với(set -e; echo 1; false; echo 2) || echo 3
Johan

Câu trả lời:


32

Theo chủ đề này , đó là hành vi POSIX chỉ định sử dụng " set -e" trong một lớp con.

(Tôi cũng ngạc nhiên.)

Đầu tiên, hành vi:

Các -ethiết lập sẽ được bỏ qua khi thực hiện danh sách hợp chất sau thời gian, cho đến khi, nếu, hoặc elif từ dành riêng, một đường ống dẫn bắt đầu với! từ dành riêng, hoặc bất kỳ lệnh nào của danh sách AND-OR khác với từ cuối cùng.

Bài viết thứ hai ghi chú,

Tóm lại, không nên đặt -e in (mã subshell) hoạt động độc lập với bối cảnh xung quanh?

Không. Mô tả POSIX rõ ràng rằng bối cảnh xung quanh ảnh hưởng đến việc tập -e có bị bỏ qua trong một lớp con hay không.

Có thêm một chút trong bài thứ tư, cũng bởi Eric Blake,

Điểm 3 không yêu cầu các lớp con để ghi đè lên các bối cảnh set -ebị bỏ qua. Đó là, một khi bạn đang ở trong một bối cảnh -ebị bỏ qua, bạn không thể làm gì để được -etuân theo một lần nữa, thậm chí không phải là một mạng con.

$ bash -c 'set -e; if (set -e; false; echo hi); then :; fi; echo $?' 
hi 
0 

Mặc dù chúng tôi đã gọi set -ehai lần (cả trong phần cha mẹ và trong phần con), thực tế là phần con tồn tại trong một bối cảnh -ebị bỏ qua (điều kiện của một câu lệnh if), chúng ta không thể làm gì trong phần con để kích hoạt lại -e.

Hành vi này chắc chắn là đáng ngạc nhiên. Nó là phản trực giác: người ta sẽ mong muốn việc kích hoạt set -elại có hiệu lực và bối cảnh xung quanh sẽ không có tiền lệ; hơn nữa, từ ngữ của tiêu chuẩn POSIX không làm cho điều này đặc biệt rõ ràng. Nếu bạn đọc nó trong ngữ cảnh mà lệnh bị lỗi, quy tắc sẽ không được áp dụng: tuy nhiên, nó chỉ áp dụng trong bối cảnh xung quanh, tuy nhiên, nó hoàn toàn áp dụng cho nó.


Cảm ơn những liên kết đó, họ đã rất thú vị. Tuy nhiên, ví dụ của tôi là (IMO) khác biệt đáng kể. Hầu hết các cuộc thảo luận đó là liệu tập hợp -e trong vỏ cha có được kế thừa bởi lớp con không : set -e; (false; echo passed;) || echo failed. Điều đó không làm tôi ngạc nhiên, thực sự, điều đó được bỏ qua trong trường hợp này do từ ngữ của tiêu chuẩn. Mặc dù vậy, trong trường hợp của tôi, tôi rõ ràng đang đặt -e trong lớp con và hy vọng lớp con thoát ra khi thất bại. Không có danh sách AND-OR trong phần phụ ...
MadSellectist

Tôi không đồng ý. Bài đăng thứ hai (tôi không thể làm cho các neo hoạt động) cho biết " Mô tả POSIX rõ ràng rằng bối cảnh xung quanh ảnh hưởng đến việc tập hợp -e có bị bỏ qua trong một khung con hay không. " - khung con nằm trong danh sách AND-OR.
Aaron D. Marasco

Bài đăng thứ tư (cũng là Erik Blake) cũng cho biết " Mặc dù chúng tôi đã gọi set -e hai lần (cả trong phần cha mẹ và phần con), thực tế là phần con tồn tại trong ngữ cảnh mà -e bị bỏ qua (điều kiện của một tuyên bố), không có gì chúng ta có thể làm trong phần con để kích hoạt lại -e. "
Aaron D. Marasco

Bạn đúng; Tôi không chắc làm thế nào tôi đọc sai chúng. Cảm ơn.
MadSellectist

1
Tôi rất vui khi biết rằng hành vi này tôi đang xé tóc ra ngoài hóa ra là trong thông số POSIX. Vậy công việc xung quanh là gì?! if||&&là truyền nhiễm? điều này thật vô lý
Steven Lu

7

Thật vậy, set -ekhông có tác dụng bên trong các subshells nếu bạn sử dụng ||toán tử sau chúng; ví dụ, điều này sẽ không hoạt động:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_1.sh: line 16: some_failed_command: command not found
# <-- inner
# <-- outer

set -e

outer() {
  echo '--> outer'
  (inner) || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Aaron D. Marasco trong câu trả lời của mình thực hiện một công việc tuyệt vời để giải thích lý do tại sao nó hành xử theo cách này.

Đây là một mẹo nhỏ có thể được sử dụng để khắc phục điều này: chạy lệnh bên trong trong nền, sau đó ngay lập tức chờ đợi nó. Nội dung waitsẽ trả về mã thoát của lệnh bên trong và bây giờ bạn đang sử dụng hàm ||sau wait, không phải hàm bên trong, do đó set -ehoạt động chính xác bên trong lệnh sau:

#!/bin/sh

# prints:
#
# --> outer
# --> inner
# ./so_2.sh: line 27: some_failed_command: command not found
# --> cleanup

set -e

outer() {
  echo '--> outer'
  inner &
  wait $! || {
    exit_code=$?
    echo '--> cleanup'
    return $exit_code
  }
  echo '<-- outer'
}

inner() {
  set -e
  echo '--> inner'
  some_failed_command
  echo '<-- inner'
}

outer

Đây là chức năng chung dựa trên ý tưởng này. Nó sẽ hoạt động trong tất cả các shell tương thích POSIX nếu bạn xóa localtừ khóa, tức là thay thế tất cả local x=ychỉ bằng x=y:

# [CLEANUP=cleanup_cmd] run cmd [args...]
#
# `cmd` and `args...` A command to run and its arguments.
#
# `cleanup_cmd` A command that is called after cmd has exited,
# and gets passed the same arguments as cmd. Additionally, the
# following environment variables are available to that command:
#
# - `RUN_CMD` contains the `cmd` that was passed to `run`;
# - `RUN_EXIT_CODE` contains the exit code of the command.
#
# If `cleanup_cmd` is set, `run` will return the exit code of that
# command. Otherwise, it will return the exit code of `cmd`.
#
run() {
  local cmd="$1"; shift
  local exit_code=0

  local e_was_set=1; if ! is_shell_attribute_set e; then
    set -e
    e_was_set=0
  fi

  "$cmd" "$@" &

  wait $! || {
    exit_code=$?
  }

  if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then
    set +e
  fi

  if [ -n "$CLEANUP" ]; then
    RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@"
    return $?
  fi

  return $exit_code
}


is_shell_attribute_set() { # attribute, like "x"
  case "$-" in
    *"$1"*) return 0 ;;
    *)    return 1 ;;
  esac
}

Ví dụ về cách sử dụng:

#!/bin/sh
set -e

# Source the file with the definition of `run` (previous code snippet).
# Alternatively, you may paste that code directly here and comment the next line.
. ./utils.sh


main() {
  echo "--> main: $@"
  CLEANUP=cleanup run inner "$@"
  echo "<-- main"
}


inner() {
  echo "--> inner: $@"
  sleep 0.5; if [ "$1" = 'fail' ]; then
    oh_my_god_look_at_this
  fi
  echo "<-- inner"
}


cleanup() {
  echo "--> cleanup: $@"
  echo "    RUN_CMD = '$RUN_CMD'"
  echo "    RUN_EXIT_CODE = $RUN_EXIT_CODE"
  sleep 0.3
  echo '<-- cleanup'
  return $RUN_EXIT_CODE
}

main "$@"

Chạy ví dụ:

$ ./so_3 fail; echo "exit code: $?"

--> main: fail
--> inner: fail
./so_3: line 15: oh_my_god_look_at_this: command not found
--> cleanup: fail
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 127
<-- cleanup
exit code: 127

$ ./so_3 pass; echo "exit code: $?"

--> main: pass
--> inner: pass
<-- inner
--> cleanup: pass
    RUN_CMD = 'inner'
    RUN_EXIT_CODE = 0
<-- cleanup
<-- main
exit code: 0

Điều duy nhất mà bạn cần lưu ý khi sử dụng phương thức này là tất cả các sửa đổi của các biến Shell được thực hiện từ lệnh bạn truyền sang runsẽ không truyền đến hàm gọi, bởi vì lệnh chạy trong một lớp con.


2

Tôi sẽ không loại trừ đó là một lỗi chỉ vì một số vỏ hoạt động theo cách đó. ;-)

Tôi có nhiều niềm vui hơn để cung cấp:

start cmd:> ( eval 'set -e'; false; echo passed; ) || echo failed
passed

start cmd:> ( eval 'set -e; false'; echo passed; ) || echo failed
failed

start cmd:> ( eval 'set -e; false; echo passed;' ) || echo failed
failed

Tôi có thể trích dẫn từ man bash (4.2.24):

Shell không thoát nếu lệnh bị lỗi là [...] một phần của bất kỳ lệnh nào được thực thi trong && hoặc || liệt kê ngoại trừ lệnh theo sau && hoặc || [...]

Có lẽ eval qua một số lệnh dẫn đến bỏ qua | | bối cảnh.


Chà, nếu tất cả các shell hoạt động theo cách đó thì theo định nghĩa không phải là lỗi ... đó là hành vi tiêu chuẩn :-). Chúng tôi có thể than thở về hành vi là không trực quan nhưng ... Thủ thuật với eval rất thú vị, đó là điều chắc chắn.
MadSellectist

Bạn dùng vỏ gì? Các evalthủ thuật không làm việc cho tôi. Tôi đã thử bash, bash ở chế độ posix và dash.
Dunatotatos

@Dunatotatos, như Hauke ​​đã nói, đó là bash4.2. Nó đã được "sửa" trong bash4.3. shell dựa trên pdksh sẽ có cùng "vấn đề". Và một số phiên bản của một số shell có tất cả các "vấn đề" khác nhau set -e. set -ebị phá vỡ bởi thiết kế. Tôi sẽ không sử dụng nó cho bất cứ điều gì ngoại trừ các tập lệnh shell đơn giản nhất mà không có cấu trúc điều khiển, lớp con hoặc thay thế lệnh.
Stéphane Chazelas

1

Cách giải quyết khi sử dụng toplevel set -e

Tôi đến với câu hỏi này vì tôi đang sử dụng set -enhư một phương pháp phát hiện lỗi:

/usr/bin/env bash
set -e
do_stuff
( take_best_sub_action_1; take_best_sub_action_2 ) || do_worse_fallback
do_more_stuff

và không có ||, kịch bản sẽ ngừng chạy và không bao giờ đạt tới do_more_stuff.

Vì dường như không có giải pháp rõ ràng, tôi nghĩ rằng tôi sẽ chỉ làm một cách đơn giản set +etrên các kịch bản của mình:

/usr/bin/env bash
set -e
do_stuff
set +e
( take_best_sub_action_1; take_best_sub_action_2 )
exit_status=$?
set -e
if [ "$exit_status" -ne 0 ]; then
  do_worse_fallback
fi
do_more_stuff
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.