Mengapa (keluar 1) tidak keluar dari skrip?

48

Saya memiliki skrip, yang tidak keluar ketika saya menginginkannya.

Contoh skrip dengan kesalahan yang sama adalah:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Saya akan berasumsi untuk melihat output:

:~$ ./test.sh
1
:~$

Tapi saya benar-benar melihat:

:~$ ./test.sh
1
2
:~$

Apakah ()perintah chaining entah bagaimana menciptakan ruang lingkup? Apa yang exitkeluar dari, jika bukan skrip?

Minix
sumber
5
Ini membutuhkan satu kata jawaban: subkulit
Yosua

Jawaban:

87

()menjalankan perintah dalam subkulit, sehingga exitAnda keluar dari subkulit dan kembali ke shell induk. Gunakan kawat gigi {}jika Anda ingin menjalankan perintah di shell saat ini.

Dari manual bash:

Daftar (daftar) dijalankan dalam lingkungan subkulit. Penugasan variabel dan perintah bawaan yang memengaruhi lingkungan shell tidak tetap berlaku setelah perintah selesai. Status kembali adalah status keluar dari daftar.

{daftar; } daftar hanya dieksekusi di lingkungan shell saat ini. daftar harus diakhiri dengan baris baru atau titik koma. Ini dikenal sebagai perintah grup. Status kembali adalah status keluar dari daftar. Perhatikan bahwa tidak seperti metacharacters (dan), {dan} adalah kata-kata yang dicadangkan dan harus muncul ketika kata yang dilindungi diizinkan untuk dikenali. Karena mereka tidak menyebabkan istirahat kata, mereka harus dipisahkan dari daftar dengan spasi putih atau karakter meta shell lainnya.

Patut disebutkan bahwa sintaks shell cukup konsisten dan subkulit berpartisipasi juga dalam ()konstruksi lain seperti substitusi perintah (juga dengan `..`sintaks gaya lama ) atau proses substitusi, sehingga yang berikut tidak akan keluar dari shell saat ini:

echo $(exit)
cat <(exit)

Meskipun mungkin jelas bahwa subkulit terlibat ketika perintah ditempatkan secara eksplisit di dalam (), fakta yang kurang terlihat adalah bahwa mereka juga muncul dalam struktur lain ini:

  • perintah dimulai di latar belakang

    exit &

    tidak keluar dari shell saat ini karena (setelah man bash)

    Jika perintah diakhiri oleh operator kontrol &, shell mengeksekusi perintah di latar belakang dalam sebuah subkulit. Shell tidak menunggu perintah selesai, dan status pengembalian adalah 0.

  • pipa

    exit | echo foo

    masih keluar hanya dari subkulit.

    Namun berbagai kerang berperilaku berbeda dalam hal ini. Misalnya bashmenempatkan semua komponen pipa ke subkulit yang terpisah (kecuali jika Anda menggunakan lastpipeopsi dalam doa di mana kontrol pekerjaan tidak diaktifkan), tetapi AT&T kshdan zshjalankan bagian terakhir di dalam shell saat ini (kedua perilaku diizinkan oleh POSIX). Jadi

    exit | exit | exit

    pada dasarnya tidak melakukan apa pun di bash, tetapi keluar dari zsh karena yang terakhir exit .

  • coproc exitjuga berjalan exitdalam subkulit.

jimmij
sumber
5
Ah. Sekarang untuk menemukan semua tempat, di mana pendahulu saya menggunakan kawat gigi yang salah. Terima kasih atas wawasannya.
Minix
10
Perhatikan spasi di halaman manual: {dan }bukan sintaks, mereka adalah kata-kata yang dicadangkan dan harus dikelilingi oleh spasi, dan daftar harus diakhiri dengan terminator perintah (titik koma, baris baru, ampersand)
glenn jackman
Karena ketertarikan, apakah ini cenderung benar-benar menjadi proses lain atau hanya lingkungan yang terpisah dalam tumpukan internal? Saya menggunakan () banyak untuk mengisolasi chdir, dan pasti beruntung dengan saya menggunakan $$ dll jika yang pertama.
Dan Sheppard
5
@DanSheppard Ini adalah proses lain, tetapi (echo $$)mencetak shell id induk karena $$diperluas bahkan sebelum subkulit dibuat. Faktanya mencetak id proses subkulit bisa jadi rumit, lihat stackoverflow.com/questions/9119885/…
jimmij
@ jimmij, Bagaimana bisa itu $$diperluas sebelum subkulit dibuat, dan belum $BASHPIDmenunjukkan nilai yang benar untuk subkulit?
Wildcard
13

Menjalankan exitsubkulit adalah satu perangkap:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Script mencetak 42, keluar dari subkulit dengan kode kembali 1, dan melanjutkan dengan skrip. Bahkan mengganti panggilan dengan echo $(CALC) || exit 1tidak membantu karena kode balik echoadalah 0 terlepas dari kode balik calc. Dan calcdieksekusi sebelum echo.

Yang lebih membingungkan adalah menggagalkan efek exitdengan membungkusnya menjadi localbuiltin seperti pada script berikut. Saya tersandung masalah ketika saya menulis fungsi untuk memverifikasi nilai input. Contoh:

Saya ingin membuat file bernama "year month day.log", yaitu 20141211.loguntuk hari ini. Tanggal tersebut diinput oleh pengguna yang mungkin gagal memberikan nilai wajar. Oleh karena itu, dalam fungsi saya, fnamesaya memeriksa nilai balik dateuntuk memverifikasi validitas input pengguna:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Kelihatan bagus. Biarkan skrip diberi nama s.sh. Jika pengguna memanggil skrip ./s.sh "Thu Dec 11 20:45:49 CET 2014", file 20141211.logdibuat. Namun, jika tipe pengguna ./s.sh "Thu hec 11 20:45:49 CET 2014", maka skrip menghasilkan:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

Baris fname…mengatakan bahwa data input buruk telah terdeteksi di subkulit. Tetapi exit 1pada akhir local …baris tidak pernah dipicu karena localarahan selalu kembali 0. Ini karena localdieksekusi setelah $(fname) dan dengan demikian menimpa kode kembali. Dan karena itu, skrip berlanjut dan memanggil touchdengan parameter kosong. Contoh ini sederhana tetapi perilaku bash bisa sangat membingungkan dalam aplikasi nyata. Saya tahu, programmer sebenarnya tidak menggunakan penduduk setempat

Untuk memperjelas: Tanpa local, skrip dibatalkan seperti yang diharapkan ketika tanggal yang tidak valid dimasukkan.

Cara mengatasinya adalah dengan membagi garis seperti

local FNAME
FNAME=$(fname "$1") || exit 1

Perilaku aneh sesuai dengan dokumentasi localdi dalam halaman manual bash: "Status pengembalian adalah 0 kecuali lokal digunakan di luar fungsi, nama tidak valid diberikan, atau nama adalah variabel readonly."

Meskipun bukan bug, saya merasa bahwa perilaku bash adalah berlawanan dengan intuisi. Saya menyadari urutan eksekusi, localtidak harus menutupi tugas yang rusak, namun.

Jawaban awal saya berisi beberapa ketidaktepatan. Setelah diskusi yang mendalam dan mendalam dengan mikeserv (terima kasih untuk itu) saya pergi untuk memperbaikinya.

hermannk
sumber
@ mikeserv: Saya menambahkan contoh untuk menunjukkan relevansi.
hermannk
@ mikeserv: Ya, Anda benar. Bahkan terser. Tapi perangkap itu masih ada.
hermannk
@ mikeserv: Maaf, contoh saya rusak. Saya lupa tes doit().
hermannk
2

Solusi aktual:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Pengelompokan kesalahan hanya akan dijalankan jika blamengembalikan status kesalahan, dan exittidak dalam subkulit sehingga seluruh skrip berhenti.

Walf
sumber
1

Tanda kurung memulai subkulit dan keluar hanya keluar dari subkulit itu.

Anda dapat membaca kode akses dengan $?dan menambahkan ini dalam skrip Anda untuk keluar dari skrip jika subshell keluar:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
rubo77
sumber