ทำไมชุด -e ไม่ทำงานภายใน subshells ด้วยวงเล็บ () ตามด้วย OR list ||?


30

ฉันใช้สคริปต์บางอย่างเช่นนี้เมื่อเร็ว ๆ นี้:

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

นี่ดูดีสำหรับฉัน แต่มันใช้งานไม่ได้! ใช้ไม่ได้เมื่อคุณเพิ่มset -e ||หากปราศจากอย่างนั้นก็ใช้งานได้ดี:

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

อย่างไรก็ตามหากฉันเพิ่มสิ่ง||นั้นset -eจะถูกละเว้น:

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

การใช้เปลือกแยกที่แท้จริงจะทำงานตามที่คาดไว้:

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

ฉันได้ลองในหลาย ๆ เชลล์ (bash, dash, ksh93) และทำตัวเหมือนกันดังนั้นจึงไม่ใช่ข้อผิดพลาด มีคนอธิบายเรื่องนี้ได้ไหม


`(.... )` `การสร้างเริ่มต้นเชลล์แยกต่างหากเพื่อเรียกใช้เนื้อหาการตั้งค่าใด ๆ ที่อยู่ในนั้นไม่ได้ใช้นอก
vonbrand

@ vonbrand คุณพลาดจุด เขาต้องการให้มันนำไปใช้ภายใน subshell แต่||subshell ภายนอกมีผลต่อพฤติกรรมภายใน subshell
cjm

1
เปรียบเทียบ(set -e; echo 1; false; echo 2)กับ(set -e; echo 1; false; echo 2) || echo 3
Johan

คำตอบ:


32

ตามหัวข้อนี้พฤติกรรมที่ POSIX ระบุไว้สำหรับการใช้ " set -e" ใน subshell

(ฉันก็ประหลาดใจเช่นกัน)

ก่อนพฤติกรรม:

การ-eตั้งค่าจะถูกละเว้นเมื่อเรียกใช้งานรายการคำสั่งผสมหลังจากคำ while, จนกว่า, ถ้า, หรือ, elif, ไปป์ไลน์เริ่มต้นด้วย! คำที่สงวนไว้หรือคำสั่งใด ๆ ของรายการ AND-OR นอกเหนือจากที่ผ่านมา

โพสต์บันทึกย่อที่สอง

โดยสรุปคุณไม่ควรตั้งค่า -e in (รหัส subshell) ให้ทำงานโดยอิสระจากบริบทแวดล้อม

ไม่คำอธิบาย POSIX ชัดเจนว่าบริบทโดยรอบมีผลต่อการตั้งค่า -e ถูกละเว้นใน subshell หรือไม่

มีอีกเล็กน้อยในโพสต์ที่สี่เช่นกันโดย Eric Blake

จุดที่ 3 ไม่ต้องการ subshells เพื่อแทนที่บริบทที่set -eถูกละเว้น นั่นคือเมื่อคุณอยู่ในบริบทที่-eถูกเพิกเฉยไม่มีอะไรที่คุณสามารถทำได้เพื่อให้-eเชื่อฟังอีกครั้งแม้กระทั่ง subshell

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

แม้ว่าเราจะเรียกset -eสองครั้ง (ทั้งในพาเรนต์และใน subshell) ความจริงที่ว่า subshell มีอยู่ในบริบทที่-eถูกละเว้น (เงื่อนไขของคำสั่ง if) ไม่มีอะไรที่เราสามารถทำได้ใน subshell เพื่อเปิดใช้งานอีกครั้ง-e.

พฤติกรรมนี้น่าประหลาดใจอย่างแน่นอน มันตอบโต้ง่าย: ใครจะคาดหวังว่าการเปิดใช้งานอีกครั้งของset -eการมีผลกระทบและว่าบริบทโดยรอบจะไม่นำหน้า; นอกจากนี้ถ้อยคำของมาตรฐาน POSIX ไม่ได้ทำให้สิ่งนี้ชัดเจนเป็นพิเศษ หากคุณอ่านในบริบทที่คำสั่งล้มเหลวกฎจะไม่นำมาใช้: จะมีผลกับบริบทโดยรอบเท่านั้น แต่จะมีผลกับกฎทั้งหมด


ขอบคุณสำหรับลิงค์เหล่านั้นพวกเขาน่าสนใจมาก อย่างไรก็ตามตัวอย่างของฉันคือ (IMO) แตกต่างอย่างมาก ส่วนใหญ่ของการอภิปรายว่าไม่ว่าจะเป็นชุดอีในเปลือกของพ่อแม่คือการสืบทอดโดย subshell set -e; (false; echo passed;) || echo failedนี้: มันไม่ได้ทำให้ฉันแปลกใจจริง ๆ ว่า -e ถูกละเว้นในกรณีนี้เนื่องจากถ้อยคำของมาตรฐาน ในกรณีของฉันฉันตั้งค่า -e ใน subshellอย่างชัดเจนและคาดว่าsubshellจะออกจากความล้มเหลว ไม่มีรายชื่อ AND-OR ใน subshell ...
MadScientist

ฉันไม่เห็นด้วย. โพสต์ที่สอง (ฉันไม่สามารถให้แองเคอร์ทำงานได้) บอกว่า " คำอธิบาย POSIX นั้นชัดเจนว่าบริบทโดยรอบมีผลต่อการตั้งค่า -e ถูกละเว้นใน subshell " - subshell อยู่ในรายการ AND-OR
Aaron D. Marasco

โพสต์ที่สี่ (เช่น Erik Blake) ก็บอกว่า " แม้ว่าเราจะเรียก set -e สองครั้ง (ทั้งใน parent และ subshell) ความจริงที่ subshell มีอยู่ในบริบทที่ e ถูกเพิกเฉย (เงื่อนไขของ if คำสั่ง) มีอะไรที่เราสามารถทำได้ใน subshell เพื่อเปิดใช้งาน -e. "
Aaron D. Marasco

คุณถูก; ฉันไม่แน่ใจว่าฉันเข้าใจผิดเหล่านั้นอย่างไร ขอบคุณ
MadScientist

1
ฉันดีใจที่ได้เรียนรู้ว่าพฤติกรรมนี้ฉันฉีกผมออกไปเป็น POSIX แล้วงานรอบตัวคืออะไร! ifและ||และ&&มีการติดเชื้อ? นี่เป็นเรื่องไร้สาระ
Steven Lu

7

แน่นอนว่าset -eจะไม่มีผลภายใน subshells ถ้าคุณใช้||โอเปอเรเตอร์หลังจากนั้น เช่นนี้ไม่ได้ผล:

#!/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 ในคำตอบของเขาทำงานได้ดีมากในการอธิบายว่าทำไมมันทำงานแบบนี้

นี่เป็นเคล็ดลับเล็กน้อยที่สามารถใช้ในการแก้ไขปัญหานี้: เรียกใช้คำสั่งด้านในเป็นพื้นหลังแล้วรอทันที waitbuiltin จะกลับรหัสทางออกของคำสั่งภายในและตอนนี้คุณกำลังใช้||หลังจากที่waitไม่ได้ฟังก์ชั่นด้านในเพื่อให้set -eทำงานอย่างถูกต้องภายในหลัง:

#!/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

นี่คือฟังก์ชั่นทั่วไปที่สร้างขึ้นจากความคิดนี้ มันควรจะทำงานในเชลล์ที่ทำงานร่วมกันได้กับ POSIX ทั้งหมดหากคุณลบlocalคำหลักเช่นแทนที่ทั้งหมดlocal x=yด้วยเพียง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
}

ตัวอย่างการใช้งาน:

#!/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 "$@"

ใช้ตัวอย่าง:

$ ./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

สิ่งเดียวที่คุณต้องระวังเมื่อใช้วิธีนี้คือการแก้ไขตัวแปร Shell ทั้งหมดที่ทำจากคำสั่งที่คุณส่งไปrunจะไม่แพร่กระจายไปยังฟังก์ชันการเรียกใช้เนื่องจากคำสั่งทำงานในเชลล์ย่อย


2

ฉันจะไม่แยกแยะมันเป็นข้อผิดพลาดเพียงเพราะกระสุนหลายตัวทำแบบนั้น ;-)

ฉันมีความสนุกมากกว่าที่จะเสนอ:

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

ฉันขออ้างอิงจาก man bash (4.2.24):

เชลล์ไม่ออกหากคำสั่งที่ล้มเหลวคือ [... ] ส่วนหนึ่งของคำสั่งใด ๆ ที่ดำเนินการใน && หรือ || รายการยกเว้นคำสั่งที่ตามหลัง && หรือสุดท้าย [ ... ]

บางที eval ในหลาย ๆ คำสั่งอาจนำไปสู่การละเว้น || บริบท.


ถ้าเชลล์ทุกตัวทำงานแบบนั้นมันไม่ได้เป็นข้อ จำกัด ... มันเป็นพฤติกรรมมาตรฐาน :-) เราอาจเสียใจกับพฤติกรรมที่ไม่เข้าใจง่าย แต่ ... เคล็ดลับของการใช้ eval นั้นน่าสนใจมากนั่นแน่นอน
MadScientist

คุณใช้เปลือกอะไร evalเคล็ดลับไม่ทำงานสำหรับฉัน ฉันพยายามทุบตีทุบตีในโหมด posix และประ
Dunatotatos

@Dunatotatos อย่างที่ Hauke ​​พูดนั่นคือ bash4.2 มันเป็น "คงที่" ใน bash4.3 เชลล์ที่ใช้ pdksh จะมี "ปัญหา" เหมือนกัน และหลายรุ่นหลายเปลือกหอยมีทุกประเภทของ "ปัญหา" set -eที่แตกต่างกันด้วย set -eถูกทำลายโดยการออกแบบ ฉันจะไม่ใช้มันเพื่อสิ่งใดนอกจากเชลล์สคริปต์ที่ง่ายที่สุดโดยไม่มีโครงสร้างการควบคุม, subshells หรือการแทนที่คำสั่ง
Stéphane Chazelas

1

วิธีหลีกเลี่ยงปัญหาเมื่อใช้ระดับบนสุด set -e

ฉันมาถึงคำถามนี้เพราะฉันใช้set -eเป็นวิธีตรวจจับข้อผิดพลาด:

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

และหากไม่มี||สคริปต์ก็จะหยุดทำงานและไม่สามารถเข้าถึงdo_more_stuffได้

เนื่องจากดูเหมือนจะไม่มีวิธีแก้ปัญหาที่สะอาดฉันคิดว่าฉันจะทำset +eสคริปต์ง่ายๆ:

/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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.