Apakah ini bug dalam bash? `return` tidak berhenti berfungsi jika dipanggil dari sebuah pipa

16

Saya mengalami beberapa masalah aneh dengan bash belakangan ini. Saat mencoba menyederhanakan skrip saya, saya membuat sepotong kecil kode ini:

$ o(){ echo | while read -r; do return 0; done; echo $?;}; o
0
$ o(){ echo | while read -r; do return 1; done; echo $?;}; o
1

returnseharusnya keluar dari fungsi tanpa mencetak $?, bukan? Baiklah, kemudian saya memeriksa apakah saya dapat kembali dari pipa sendirian:

$ echo | while read -r; do return 1; done
bash: return: can only `return' from a function or sourced script

Hal yang sama terjadi tanpa whileloop:

$ foo(){ : | return 1; echo "This should not be printed.";}
$ foo
This should not be printed.

Apakah ada sesuatu yang saya lewatkan di sini? Pencarian Google tidak menghasilkan apa-apa tentang ini! Versi bash saya adalah 4.2.37 (1) - dirilis di Debian Wheezy.

Teresa e Junior
sumber
Adakah yang salah dengan pengaturan yang saya sarankan dalam balasan saya yang memungkinkan skrip Anda berperilaku seperti yang Anda harapkan?
Jlliagre
@ jlliagre Ini adalah skrip yang agak rumit pada ribuan baris. Dengan kekhawatiran merusak sesuatu yang lain, saya lebih suka menghindari menjalankan pipa di dalam fungsi, jadi saya menggantinya dengan proses substitusi. Terima kasih!
Teresa e Junior
Mengapa tidak menghapus dua contoh pertama, jika whiletidak diperlukan untuk reproduksi? Itu mengalihkan perhatian dari intinya.
Lightness Races dengan Monica
@LightnessRacesinOrbit whileLoop adalah penggunaan yang sangat umum untuk pipa return. Contoh kedua lebih langsung ke titik, tetapi itu adalah sesuatu yang saya tidak percaya ada yang akan menggunakan ...
Teresa e Junior
1
Sayangnya jawaban saya yang benar telah dihapus ... Anda berada di zona abu-abu saat Anda melakukan sesuatu yang tidak ditentukan. Perilaku ini tergantung pada bagaimana shell mengartikan pipa dan ini bahkan berbeda antara Bourne Shell dan Korn Shell meskipun ksh berasal dari sumber sh. Dalam Shell Bourne, loop sementara berada dalam subshell karena itu Anda melihat gema dengan bash, Dalam ksh loop sementara adalah proses foreground dan dengan demikian ksh tidak memanggil gema dengan contoh Anda.
schily

Jawaban:

10

Terkait: /programming//a/7804208/4937930

Ini bukan bug yang Anda tidak bisa keluar dari skrip atau kembali dari fungsi oleh exitatau returndalam subshell. Mereka dieksekusi dalam proses lain dan tidak mempengaruhi proses utama.

Selain itu, saya kira Anda melihat perilaku bash tidak terdokumentasi pada (mungkin) spec yang tidak terdefinisi. Dalam suatu fungsi, tidak ada kesalahan yang ditegaskan returndi tingkat atas perintah subshell dan itu hanya berperilaku seperti exit.

IMHO itu bug bash untuk perilaku tidak konsisten returntergantung pada apakah pernyataan utama dalam suatu fungsi atau tidak.

#!/bin/bash

o() {
    # Runtime error, but no errors are asserted,
    # each $? is set to the return code.
    echo | return 10
    echo $?
    (return 11)
    echo $?

    # Valid, each $? is set to the exit code.
    echo | exit 12
    echo $?
    (exit 13)
    echo $?
}
o

# Runtime errors are asserted, each $? is set to 1.
echo | return 20
echo $?
(return 21)
echo $?

# Valid, each $? is set to the exit code.
echo | exit 22
echo $?
(exit 23)
echo $?

Keluaran:

$ bash script.sh 
10
11
12
13
script.sh: line 20: return: can only `return' from a function or sourced script
1
script.sh: line 22: return: can only `return' from a function or sourced script
1
22
23
yaegashi
sumber
Kurangnya kesalahan verbositas mungkin tidak berdokumen. Tetapi fakta yang returntidak bekerja dari urutan perintah tingkat atas dalam subkulit, dan khususnya tidak keluar dari subkulit, adalah apa yang sudah membuat saya berharap dokumen yang ada. OP dapat menggunakan exit 1 || return 1tempat mereka mencoba menggunakan return, dan kemudian harus mendapatkan perilaku yang diharapkan. EDIT: @ herbert jawaban menunjukkan bahwa tingkat atas returndalam subkulit berfungsi sebagai exit(tetapi hanya dari subkulit).
dubiousjim
1
@dubiousjim Memperbarui skrip saya. Maksud saya returndalam urutan subshell sederhana harus dinyatakan sebagai kesalahan runtime dalam kasus apa pun , tetapi sebenarnya tidak ketika itu terjadi di sebuah fucntion. Masalah ini juga telah dibahas di gnu.bash.bug , tetapi tidak ada kesimpulan.
yaegashi
1
Jawaban Anda tidak benar karena tidak ditentukan apakah loop sementara berada dalam subkulit atau proses foreground. Terlepas dari implementasi shell yang sebenarnya, returnpernyataan itu dalam fungsi dan dengan demikian legal. Namun perilaku yang dihasilkan tidak ditentukan.
schily
Anda tidak boleh menulis itu adalah perilaku tidak berdokumen sedangkan komponen pipa fakta dalam subkulit didokumentasikan di halaman manual bash. Anda tidak boleh menulis perilaku yang mungkin didasarkan pada spesifikasi yang tidak terdefinisi sementara POSIX menentukan perilaku yang diperbolehkan. Anda tidak boleh mencurigai bug bash saat bash mengikuti standar POSIX dengan mengizinkan pengembalian fungsi tetapi tidak di luar.
jlliagre
17

Ini bukan bug di dalam bashtetapi perilaku yang didokumentasikan :

Setiap perintah dalam sebuah pipa dieksekusi dalam subkulitnya sendiri

The returninstruksi makhluk berlaku di dalam definisi fungsi tetapi berada di subkulit juga, itu tidak mempengaruhi shell induknya sehingga instruksi berikutnya, echo, dijalankan terlepas. Namun demikian, ini adalah konstruksi shell yang tidak portabel karena standar POSIX memungkinkan perintah-perintah yang menyusun pipa untuk dieksekusi baik dalam subkulit (default) atau yang atas (ekstensi yang diizinkan).

Selain itu, setiap perintah pipa multi-perintah berada dalam lingkungan subkulit; sebagai ekstensi, bagaimanapun, setiap atau semua perintah dalam pipa dapat dieksekusi di lingkungan saat ini. Semua perintah lain harus dijalankan di lingkungan shell saat ini.

Semoga, Anda dapat memberi tahu bashuntuk berperilaku seperti yang Anda harapkan dengan beberapa pilihan:

$ set +m # disable job control
$ shopt -s lastpipe # do not run the last command of a pipeline a subshell 
$ o(){ echo | while read -r; do return 0; done; echo $?;}
$ o
$          <- nothing is printed here
Jlliagre
sumber
1
Karena returntidak akan keluar dari fungsi, tidakkah akan lebih masuk akal jika shell hanya dicetak bash: return: can only `return' from a function or sourced script, alih-alih memberi pengguna perasaan palsu bahwa fungsi tersebut mungkin telah kembali?
Teresa e Junior
2
Saya tidak melihat di mana pun dalam dokumentasi yang mengatakan bahwa pengembalian di dalam subkulit valid. Saya yakin bahwa fitur ini disalin dari ksh, pernyataan pengembalian fungsi luar atau skrip bersumber berperilaku seperti keluar . Saya tidak yakin tentang shell Bourne asli.
cuonglm
1
@ jlliagre: Mungkin Teresa bingung tentang terminologi dari apa yang dia minta, tapi saya tidak mengerti mengapa itu akan menjadi "rumit" untuk bash untuk mengeluarkan diagnostik jika Anda mengeksekusi returndari subkulit. Setelah semua, ia tahu bahwa itu dalam subkulit, sebagaimana dibuktikan oleh $BASH_SUBSHELLvariabel. Masalah terbesar adalah bahwa ini dapat mengarah pada positif palsu; seorang pengguna yang mengerti bagaimana subkulit bekerja dapat memiliki skrip tertulis yang digunakan returnsebagai pengganti exituntuk mengakhiri sebuah subkulit. (Dan, tentu saja, ada kasus yang valid di mana orang mungkin ingin mengatur variabel atau melakukan cdsubkulit.)
Scott
1
@Scott Saya pikir saya mengerti situasinya dengan baik. Sebuah pipa membuat subkulit, dan returnkembali dari subkulit bukannya gagal, karena berada di dalam fungsi aktual. Masalahnya adalah bahwa help returnsecara khusus menyatakan: Causes a function or sourced script to exit with the return value specified by N.Dari membaca dokumentasi, setiap pengguna akan mengharapkannya setidaknya gagal atau mencetak peringatan, tetapi tidak pernah berperilaku seperti exit.
Teresa e Junior
1
Tampak bagi saya bahwa siapa pun yang mengharapkan return dalam subkulit dalam fungsi untuk kembali dari fungsi (dalam proses shell utama) tidak mengerti subkulit dengan sangat baik. Sebaliknya, saya akan mengharapkan pembaca yang mengerti subkulit untuk mengharapkan return dalam subkulit dalam fungsi untuk mengakhiri subkulit, sama seperti yang exitakan.
Scott
6

Per dokumentasi POSIX, menggunakan di returnluar fungsi atau skrip bersumber tidak ditentukan . Jadi, itu tergantung pada cangkang Anda untuk menangani.

Shell SystemV akan melaporkan kesalahan, sementara di ksh, di returnluar fungsi atau skrip bersumber seperti exit. Kebanyakan shell POSIX dan osh schily juga berperilaku seperti itu:

$ for s in /bin/*sh /opt/schily/bin/osh; do
  printf '<%s>\n' $s
  $s -c '
    o(){ echo | while read l; do return 0; done; echo $?;}; o
  '
done
</bin/bash>
0
</bin/dash>
0
</bin/ksh>
</bin/lksh>
0
</bin/mksh>
0
</bin/pdksh>
0
</bin/posh>
0
</bin/sh>
0
</bin/yash>
0
</bin/zsh>
</opt/schily/bin/osh>
0

kshdan zshtidak menghasilkan karena bagian terakhir dari pipa di shell ini dieksekusi di shell saat ini, bukan subshell. Pernyataan pengembalian mempengaruhi lingkungan shell saat ini yang disebut fungsi, menyebabkan fungsi kembali segera tanpa mencetak apa pun.

Dalam sesi interaktif, bashhanya laporkan kesalahan tetapi tidak menghentikan shell, schily's oshmelaporkan kesalahan dan menghentikan shell:

$ for s in /bin/*sh; do printf '<%s>\n' $s; $s -ci 'return 1; echo 1'; done
</bin/bash>
bash: return: can only `return' from a function or sourced script
1
</bin/dash>
</bin/ksh>
</bin/lksh>
</bin/mksh>
</bin/pdksh>
</bin/posh>
</bin/sh>
</bin/yash>
</bin/zsh>
</opt/schily/bin/osh>
$ cannot return when not in function

( zshdalam sesi interaktif dan output terminal tidak dihentikan bash,, yashdan schily's oshmelaporkan kesalahan tetapi tidak menghentikan shell)

cuonglm
sumber
1
Dapat diperdebatkan returndigunakan di dalam suatu fungsi di sini.
jlliagre
1
@ jlliagre: Tidak yakin apa yang Anda maksud, returndigunakan di dalam fungsi subshell inside , kecuali dan . kshzsh
cuonglm
2
Maksud saya berada di dalam subkulit yang berada di dalam suatu fungsi tidak selalu berarti berada di luar fungsi itu, yaitu tidak ada dalam komponen standar menyatakan komponen pipa harus dianggap berada di luar fungsi di mana mereka berada. Ini layak untuk diklarifikasi oleh Grup Terbuka.
jlliagre
3
Saya pikir tidak. Itu di luar fungsi. Shell yang disebut fungsi dan subkulit yang dieksekusi kembali berbeda.
cuonglm
Saya mengerti alasan Anda yang dengan tepat menjelaskan masalah ini, maksud saya adalah sesuai dengan tata bahasa shell yang dijelaskan dalam standar POSIX, pipa adalah bagian dari daftar-senyawa yang merupakan bagian dari perintah-majemuk yang merupakan badan fungsi. Tidak disebutkan di mana komponen pipa harus dipertimbangkan di luar fungsi. Sama seperti jika saya di dalam mobil dan mobil itu diparkir di garasi, saya dapat berasumsi saya di garasi itu juga ;-)
jlliagre
4

Saya pikir Anda mendapatkan perilaku yang diharapkan, dalam bash, setiap perintah dalam pipa dieksekusi dalam subkulit. Anda dapat mengabdikan diri dengan mencoba mengubah variabel global fungsi Anda:

foo(){ x=42; : | x=3; echo "x==$x";}

Ngomong-ngomong, kembalinya bekerja tetapi kembali dari subkulit. Sekali lagi Anda dapat memeriksa bahwa:

foo(){ : | return 1; echo$?; echo "This should not be printed.";}

Akan menampilkan yang berikut:

1
This should not be printed.

Jadi, pernyataan pengembalian dengan benar keluar dari subkulit

.

herbert
sumber
2
Karenanya, untuk keluar dari fungsi, gunakan foo(){ : | return 1 || return 2; echo$?; echo "This should not be printed.";}; foo; echo $?dan Anda akan mendapatkan hasil 2. Tapi untuk kejelasan saya akan membuat return 1be exit 1.
dubiousjim
Ngomong-ngomong, apakah ada beberapa pembenaran atas fakta bahwa semua anggota pipa (tidak semua kecuali satu) dieksekusi dalam subkulit?
Incnis Mrsi
@IncnisMrsi: Lihat jawaban jlliagre .
Scott
1

Jawaban yang lebih umum adalah bahwa bash dan beberapa cangkang lain biasanya meletakkan semua elemen pipa ke dalam proses yang terpisah. Ini wajar ketika baris perintah

program 1 | program 2 | program 3

karena program biasanya dijalankan dalam proses yang terpisah pula (kecuali jika Anda mengatakan ). Tapi itu bisa mengejutkanexec program

perintah 1 | perintah 2 | perintah 3

di mana beberapa atau semua perintah adalah perintah bawaan. Contoh sepele meliputi:

$ a=0
$ echo | a=1
$ echo "$a"
0
$ cd /
$ echo | cd /tmp
$ pwd
/

Contoh yang sedikit lebih realistis adalah

$ t=0
$ ps | while read pid rest_of_line
> do
>     : $((t+=pid))
> done
$ echo "$t"
0

di mana seluruh while... do... doneloop dimasukkan ke dalam subproses, dan perubahannya menjadi ttidak terlihat oleh shell utama setelah loop berakhir. Dan itulah yang Anda lakukan - memipis ke dalam sebuah whileloop, menyebabkan loop berjalan sebagai subkulit, dan kemudian mencoba untuk kembali dari subkulit.

Scott
sumber