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.