Neden set -e, parantez () ve ardından bir OR listesi bulunan alt kabukların içinde çalışmıyor?


30

Son zamanlarda böyle bir komut dosyası ile karşılaştı:

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

Bu bana iyi görünüyor, ama çalışmıyor! set -eEklediğinizde, geçerli değildir ||. Bu olmadan, iyi çalışıyor:

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

Ben eklerseniz Ancak ||, set -egöz ardı edilir:

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

Gerçek, ayrı bir kabuk kullanılması beklendiği gibi çalışır:

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

Bunu birçok farklı kabukta (bash, dash, ksh93) denedim ve hepsi aynı şekilde davrandı, bu yüzden bir hata değil. Birisi bunu açıklayabilir mi?


`(....)` `yapı içeriğini çalıştırmak için ayrı bir kabuk başlatır, içindeki herhangi bir ayar dışarıda uygulanmaz.
vonbrand

@ vonbrand, noktayı kaçırdınız. Alt kabuğun içine uygulanmasını istiyor, ancak ||alt kabuğun dış kısmı alt kabuğun içindeki davranışı etkiliyor.
cjm

1
Karşılaştırma (set -e; echo 1; false; echo 2)ile(set -e; echo 1; false; echo 2) || echo 3
Johan

Yanıtlar:


32

Bu konuya göre , set -ePOSIX'in bir alt kabukta " " kullanımı için belirlediği davranış budur .

(Ben de şaşırdım.)

İlk olarak, davranış:

Ayarlanan -elisteyi,! ayrılmış sözcük veya sonuncusu dışındaki bir AND-OR listesinin herhangi bir komutu.

İkinci gönderi notları,

Özetle, -e (alt kabuk kodu) 'u çevreleyen içerikten bağımsız olarak çalıştırılmamalıdır mı?

Hayır. POSIX açıklaması, çevreleyen bağlamın set -e'nin bir alt kabukta yoksayılmamasını etkilediği açıktır.

Dördüncü yazıda biraz daha var, ayrıca Eric Blake,

Nokta 3 edilir değil bağlamları geçersiz kılmak için altkabuklarda gerektiren set -egöz ardı edilir. Yani, bir kez -egöz ardı edilmiş bir bağlamda olduğunuzda, bir alt kabuk bile değil, tekrar uymak için yapabileceğiniz hiçbir şey yoktur -e.

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

set -eİki kere de çağırılmış olsak da (hem ana hem de alt kabukta), alt kabuğun -egöz ardı edilen bir bağlamda mevcut olması (bir if ifadesinin koşulu), alt kabukta yeniden etkinleştirmek için yapabileceğimiz hiçbir şey yoktur. -e.

Bu davranış kesinlikle şaşırtıcı. Karşı sezgiseldir: Biri yeniden etkinliğin set -ebir etkisinin oluşmasını bekler ve çevresindeki bağlamın önceliğini almaz; ayrıca, POSIX standardının ifadesi bunu özellikle netleştirmez. Komutun başarısız olduğu bağlamda okursanız, kural geçerli değildir: yalnızca çevreleyen bağlamda geçerlidir, ancak tamamen buna uygulanır.


Bu bağlantılar için teşekkürler, çok ilginçti. Ancak benim örneğim (IMO) oldukça farklı. Bu tartışmanın çoğu, -e'nin ana kabuğundaki sete'nin alt kabuk tarafından miras alınmış olup olmadığıdır set -e; (false; echo passed;) || echo failed. Beni şaşırtmadı, aslında, standardın ifadesi verilen bu durumda göz ardı ediliyor. Ancak benim durumumda, açıkça alt kabukta -e ayarını yapıyorum ve alt kabuktan başarısızlıktan çıkmasını bekliyorum . Denizaltıda AND-OR listesi yok ...
MadScientist

Katılmıyorum. İkinci gönderi (çalışacak çapaları alamıyorum) " POSIX açıklaması çevreleyen içeriğin set -e'nin bir alt kabukta yok sayılmasının etkilenmeyeceğinden etkilendiği açıktır . " - alt kabuk AND-OR listesindedir.
Aaron D. Marasco,

Dördüncü posta (ayrıca Erik Blake) ayrıca “ e-setini iki kere çağırmamıza rağmen (hem ebeveyn hem de alt kabukta), alt kabuğun göz ardı edildiği bir bağlamda var olduğu gerçeğini (örneğin bir Açıklama), denizaltıda -e'yi tekrar etkinleştirmek için yapabileceğimiz bir şey yok. "
Aaron D. Marasco

Haklısın; Bunları nasıl yanlış anladığımdan emin değilim. Teşekkürler.
MadScientist

1
Saçlarımı yırtdığım bu davranışın POSIX'e özgü olduğunu öğrendiğim için çok mutluyum. Öyleyse buralarda ne iş var? ifve ||ve &&bulaşıcı mı? Bu çok saçma
Steven Lu

7

Nitekim, operatörü onlardan sonra set -ekullanırsanız, alt kabukların içinde bir etkisi olmaz ||; örneğin, bu işe yaramaz:

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

Cevabındaki Aaron D. Marasco , neden bu şekilde davrandığını açıklamak için harika bir iş çıkardı.

İşte bunu düzeltmek için kullanılabilecek küçük bir püf noktası: iç komutu arka planda çalıştırın ve ardından hemen bekleyin. waitYerleşik iç komutun çıkış kodu döndürür ve şimdi kullandığınız ||sonra waitöylesine değil, iç fonksiyonu, set -eikincisi içeride düzgün çalışır:

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

İşte bu fikir üzerine kurulu genel fonksiyon. localAnahtar kelimeleri kaldırırsanız tüm POSIX uyumlu mermilerde çalışması gerekir , örneğin hepsini local x=ysadece ile değiştirin 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
}

Kullanım örneği:

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

Örnek çalıştırılıyor:

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

Bu yöntemi kullanırken bilmeniz gereken tek şey, ilettiğiniz komuttan yapılan Shell değişkenlerinin tüm değişikliklerinin, komut runişlevi bir alt kabukta çalıştığı için çağıran işleve yayılmayacak olmasıdır.


2

Bunun bir hata olduğunu göz ardı etmem, çünkü birkaç mermi bu şekilde davranır. ;-)

Size daha fazla eğlencem var:

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

Bash (4.2.24) 'den alıntı yapabilir miyim:

Bir && veya || 'de yürütülen komutun [...] parçası başarısız olursa, komut başarısız olursa kabuk çıkmaz. final && veya || [...]

Belki de birkaç komut üzerinden yapılan değerlendirme || bağlamı.


Peki, bütün mermiler bu şekilde davranırsa, tanım gereği bir hata değil ... standart bir davranıştır :-). Davranışı sezgisel olmayan olarak nitelendirebiliriz ama ... Değerlendirmeyle ilgili numara çok ilginç, orası kesin.
MadScientist,

Hangi kabuğu kullanıyorsunuz? İşin evalpüf noktası benim için çalışmıyor. Bash, posix modunda bash ve çizgi de denedim.
Dunatotatos 12:17

@Dunatotatos, Hauke'un dediği gibi, bu bash4.2 idi. Bash4.3'te "düzeltildi". pdksh tabanlı mermiler aynı "sayıya" sahip olacak. Ve birkaç merminin çeşitli versiyonlarında, her türlü farklı "sorun" vardır set -e. set -etasarım tarafından bozuldu. Kontrol yapıları, alt kabuklar veya komut değişimleri olmayan en basit kabuk komut dosyaları için kullanmazdım.
Stéphane Chazelas

1

Üst düzey kullanımda geçici çözüm set -e

Bu soruya geldim çünkü set -ehata tespit metodu olarak kullanıyordum :

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

ve onsuz ||, senaryo çalışmayı bırakıp asla erişemezdi do_more_stuff.

Temiz bir çözüm yok gibi gözüktüğünden, set +esenaryolarımda basit bir işlem yapacağımı düşünüyorum :

/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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.