Mengapa set -e tidak berfungsi di dalam subkulit dengan tanda kurung () diikuti dengan daftar ATAU ||?


30

Saya telah menemukan beberapa skrip seperti ini baru-baru ini:

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

Ini terlihat baik bagi saya, tetapi tidak berhasil! Tidak set -eberlaku, saat Anda menambahkan ||. Tanpa itu, ia bekerja dengan baik:

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

Namun, jika saya menambahkan ||, set -ediabaikan:

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

Menggunakan shell nyata yang terpisah berfungsi seperti yang diharapkan:

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

Saya sudah mencoba ini di beberapa shell yang berbeda (bash, dash, ksh93) dan semua berperilaku dengan cara yang sama, jadi itu bukan bug. Adakah yang bisa menjelaskan hal ini?


Konstruk `(....)` `memulai shell terpisah untuk menjalankan kontennya, pengaturan apa pun di dalamnya tidak berlaku di luar.
vonbrand

@vonbrand, Anda melewatkan intinya. Dia ingin itu berlaku di dalam subkulit, tetapi di ||luar subkulit mempengaruhi perilaku di dalam subkulit.
cjm

1
Bandingkan (set -e; echo 1; false; echo 2)dengan(set -e; echo 1; false; echo 2) || echo 3
Johan

Jawaban:


32

Menurut utas ini , itu adalah perilaku yang ditentukan POSIX untuk menggunakan " set -e" dalam sebuah subkulit.

(Saya terkejut juga.)

Pertama, perilaku:

The -ePengaturan harus diabaikan ketika menjalankan daftar senyawa berikut sementara, sampai, jika, atau Elif kata reserved, pipa dimulai dengan! kata yang dipesan, atau perintah apa pun dari daftar AND-OR selain yang terakhir.

Catatan posting kedua,

Singkatnya, bukankah seharusnya set -e in (kode subkulit) beroperasi secara independen dari konteks sekitarnya?

Tidak. Deskripsi POSIX jelas bahwa konteks sekitarnya mempengaruhi apakah set -e diabaikan dalam subkulit.

Ada lebih banyak di pos keempat, juga oleh Eric Blake,

Poin 3 tidak memerlukan subkulit untuk menimpa konteks di mana set -ediabaikan. Artinya, begitu Anda berada dalam konteks di mana -ediabaikan, tidak ada yang dapat Anda lakukan untuk -edipatuhi lagi, bahkan tidak ada subkulit.

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

Meskipun kami memanggil set -edua kali (baik dalam induk dan dalam subkulit), fakta bahwa subkulit ada dalam konteks di mana -ediabaikan (kondisi pernyataan if), tidak ada yang bisa kita lakukan dalam subkulit untuk mengaktifkan kembali -e.

Perilaku ini jelas mengejutkan. Ini kontra-intuitif: orang akan mengharapkan dimulainya kembali set -euntuk memiliki efek, dan bahwa konteks sekitarnya tidak akan menjadi preseden; lebih jauh, kata-kata dari standar POSIX tidak membuat ini sangat jelas. Jika Anda membacanya dalam konteks di mana perintah gagal, aturannya tidak berlaku: itu hanya berlaku dalam konteks sekitarnya, namun, itu berlaku sepenuhnya.


Terima kasih atas tautan itu, mereka sangat menarik. Namun, contoh saya (IMO) secara substansial berbeda. Sebagian besar diskusi yang adalah apakah set -e di shell orang tua diwarisi oleh subkulit: set -e; (false; echo passed;) || echo failed. Tidak mengherankan saya, sebenarnya, bahwa -e diabaikan dalam hal ini mengingat kata-kata standar. Dalam kasus saya, saya secara eksplisit menetapkan -e di subkulit , dan mengharapkan subkulit untuk keluar pada kegagalan. Tidak ada daftar AND-OR dalam subkulit ...
MadScientist

Saya tidak setuju. Posting kedua (saya tidak dapat mengaktifkan anchor) mengatakan " Deskripsi POSIX jelas bahwa konteks di sekitarnya memengaruhi apakah set -e diabaikan dalam sebuah subkulit. " - subkulit tersebut ada dalam daftar AND-OR.
Aaron D. Marasco

Pos keempat (juga Erik Blake) juga mengatakan " Meskipun kita memanggil set -e dua kali (baik di induk maupun di subkulit), fakta bahwa subkulit ada dalam konteks di mana -e diabaikan (kondisi jika pernyataan), tidak ada yang bisa kita lakukan dalam subkulit untuk mengaktifkan kembali -e. "
Aaron D. Marasco

Kamu benar; Saya tidak yakin bagaimana saya salah membaca itu. Terima kasih.
MadScientist

1
Saya senang mengetahui bahwa perilaku ini saya mencabuti rambut saya ternyata dalam spec POSIX. Jadi apa yang harus dilakukan ?! ifdan ||dan &&apakah menular? ini tidak masuk akal
Steven Lu

7

Memang, set -etidak ada efek di dalam subkulit jika Anda menggunakan ||operator setelahnya; mis. ini tidak akan berfungsi:

#!/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 dalam jawabannya melakukan pekerjaan yang baik untuk menjelaskan mengapa itu berlaku seperti ini.

Berikut ini adalah sedikit trik yang dapat digunakan untuk memperbaikinya: jalankan perintah dalam di latar belakang, dan kemudian segera tunggu. The waitbuiltin akan mengembalikan kode keluar dari perintah batin, dan sekarang Anda menggunakan ||setelah wait, bukan fungsi batin, jadi set -ebekerja dengan baik dalam kedua:

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

Inilah fungsi generik yang dibangun di atas gagasan ini. Ini harus bekerja di semua shell yang kompatibel dengan POSIX jika Anda menghapus localkata kunci, yaitu ganti semua local x=yhanya dengan 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
}

Contoh penggunaan:

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

Menjalankan contoh:

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

Satu-satunya hal yang perlu Anda perhatikan ketika menggunakan metode ini adalah bahwa semua modifikasi variabel Shell yang dilakukan dari perintah yang Anda lewati runtidak akan merambat ke fungsi panggilan, karena perintah berjalan dalam subkulit.


2

Saya tidak akan mengesampingkan itu bug hanya karena beberapa shell berperilaku seperti itu. ;-)

Saya memiliki lebih banyak kesenangan untuk ditawarkan:

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

Bolehkah saya mengutip dari man bash (4.2.24):

Shell tidak keluar jika perintah yang gagal adalah [...] bagian dari perintah apa pun yang dieksekusi di && atau || daftar kecuali perintah mengikuti akhir && atau || [...]

Mungkin eval atas beberapa perintah menyebabkan mengabaikan || konteks.


Nah, jika semua shell berperilaku seperti itu secara definisi bukan bug ... itu perilaku standar :-). Kita mungkin menyesali perilaku itu sebagai tidak intuitif tetapi ... Trik dengan eval sangat menarik, itu sudah pasti.
MadScientist

Shell apa yang Anda gunakan? The evaltrick tidak bekerja untuk saya. Saya mencoba bash, bash dalam mode posix, dan dash.
Dunatotatos

@Dunatotatos, seperti yang dikatakan Hauke, itu bash4.2. Itu "diperbaiki" di bash4.3. shell berbasis pdksh akan memiliki "masalah" yang sama. Dan beberapa versi dari beberapa shell memiliki segala macam "masalah" dengan set -e. set -erusak oleh desain. Saya tidak akan menggunakannya untuk apa pun kecuali skrip shell paling sederhana tanpa struktur kontrol, subkulit, atau penggantian perintah.
Stéphane Chazelas

1

Penanganan masalah saat kami tingkat atas set -e

Saya sampai pada pertanyaan ini karena saya menggunakan set -emetode deteksi kesalahan:

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

dan tanpa ||, skrip akan berhenti berjalan dan tidak pernah mencapai do_more_stuff.

Karena tampaknya tidak ada solusi bersih, saya pikir saya hanya akan melakukan sederhana set +epada skrip saya:

/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
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.