Output substitusi proses tidak sesuai pesanan

16

Itu

echo one; echo two > >(cat); echo three; 

perintah memberikan output yang tak terduga.

Saya membaca ini: Bagaimana proses substitusi diimplementasikan dalam bash? dan banyak artikel lain tentang substitusi proses di internet, tetapi tidak mengerti mengapa ia berperilaku seperti ini.

Output yang diharapkan:

one
two
three

Output nyata:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

Juga, kedua perintah ini harus setara dari sudut pandang saya, tetapi tidak:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

Mengapa saya berpikir, mereka harus sama? Karena, keduanya menghubungkan seqoutput ke catinput melalui pipa anonim - Wikipedia, Substitusi proses .

Pertanyaan: Mengapa berperilaku seperti ini? Di mana kesalahan saya? Jawaban komprehensif diinginkan (dengan penjelasan bagaimana bashmelakukannya di bawah tenda).

MiniMax
sumber
2
Bahkan jika itu tidak begitu jelas pada pandangan pertama, itu sebenarnya duplikat dari bash menunggu proses dalam proses substitusi bahkan jika perintah tidak valid
Stéphane Chazelas
2
Sebenarnya, akan lebih baik jika pertanyaan lain itu ditandai sebagai duplikat untuk yang satu ini karena yang ini lebih penting. Itulah sebabnya saya menyalin jawaban saya di sana.
Stéphane Chazelas

Jawaban:

21

Ya, bashseperti di ksh( di mana fitur tersebut berasal), proses di dalam proses substitusi tidak menunggu (sebelum menjalankan perintah berikutnya dalam skrip).

untuk <(...)satu, itu biasanya baik seperti di:

cmd1 <(cmd2)

shell akan menunggu cmd1dan cmd1biasanya akan menunggu cmd2berdasarkan pembacaan sampai akhir file pada pipa yang diganti, dan bahwa end-file biasanya terjadi ketika cmd2mati. Itulah alasan yang sama mengapa beberapa kerang (tidak bash) tidak perlu menunggu untuk cmd2masuk cmd2 | cmd1.

Untuk cmd1 >(cmd2), bagaimanapun, bahwa umumnya tidak terjadi, karena lebih cmd2yang biasanya menunggu cmd1di sana sehingga umumnya akan keluar setelah.

Itu sudah diperbaiki zshyang menunggu di cmd2sana (tetapi tidak jika Anda menuliskannya sebagai cmd1 > >(cmd2)dan cmd1bukan bawaan, gunakan {cmd1} > >(cmd2)sebagai gantinya yang didokumentasikan ).

kshtidak menunggu secara default, tetapi memungkinkan Anda menunggu dengan waitbuiltin (itu juga membuat pid tersedia $!, meskipun itu tidak membantu jika Anda melakukannya cmd1 >(cmd2) >(cmd3))

rc(dengan cmd1 >{cmd2}sintaks), sama seperti kshkecuali Anda bisa mendapatkan semua proses latar belakang dengan $apids.

es(juga dengan cmd1 >{cmd2}) menunggu cmd2like in zsh, dan juga menunggu redirections cmd2dalam <{cmd2}proses.

bashmemang membuat pid dari cmd2(atau lebih tepatnya dari subkulit seperti itu berjalan cmd2dalam proses anak dari subkulit itu meskipun itu adalah perintah terakhir di sana) tersedia di $!, tetapi tidak membiarkan Anda menunggu untuk itu.

Jika Anda harus menggunakan bash, Anda dapat mengatasi masalah dengan menggunakan perintah yang akan menunggu kedua perintah dengan:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

Itu membuat keduanya cmd1dan cmd2memiliki fd 3 terbuka untuk sebuah pipa. catakan menunggu akhir file di ujung yang lain, jadi biasanya hanya akan keluar ketika keduanya cmd1dan cmd2sudah mati. Dan shell akan menunggu catperintah itu. Anda dapat melihat bahwa sebagai jaring untuk menangkap penghentian semua proses latar belakang (Anda dapat menggunakannya untuk hal-hal lain yang dimulai di latar belakang seperti dengan &, coprocs atau bahkan perintah yang melatarbelakangi diri mereka sendiri asalkan mereka tidak menutup semua deskriptor file mereka seperti yang biasanya dilakukan daemon ).

Perhatikan bahwa berkat proses subkulit sia-sia yang disebutkan di atas, ia bekerja bahkan jika cmd2menutup fd 3-nya (perintah biasanya tidak melakukan itu, tetapi beberapa suka sudoatau sshtidak). Versi masa depan pada bashakhirnya dapat melakukan optimasi di sana seperti di shell lain. Maka Anda akan membutuhkan sesuatu seperti:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

Untuk memastikan masih ada proses shell tambahan dengan fd 3 yang terbuka menunggu sudoperintah itu.

Catatan yang cattidak akan membaca apa pun (karena proses tidak menulis di fd 3 mereka). Itu hanya ada untuk sinkronisasi. Ini akan melakukan hanya satu read()panggilan sistem yang akan kembali tanpa hasil di akhir.

Anda sebenarnya dapat menghindari berjalan catdengan menggunakan substitusi perintah untuk melakukan sinkronisasi pipa:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

Kali ini, itu shell bukan catyang membaca dari pipa yang ujungnya terbuka pada fd 3 dari cmd1dan cmd2. Kami menggunakan tugas variabel sehingga status keluar dari cmd1tersedia di $?.

Atau Anda bisa melakukan proses substitusi dengan tangan, dan kemudian Anda bahkan bisa menggunakan sistem Anda shkarena itu akan menjadi sintaksis shell standar:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

meskipun perhatikan seperti disebutkan sebelumnya bahwa tidak semua sh implementasi akan menunggu cmd1setelah cmd2selesai (meskipun itu lebih baik daripada sebaliknya). Waktu itu, $?berisi status keluar dari cmd2; meskipun bashdan zshmembuat cmd1status keluar tersedia di ${PIPESTATUS[0]}dan $pipestatus[1]masing - masing (lihat juga pipefailopsi dalam beberapa shell sehingga $?dapat melaporkan kegagalan komponen pipa selain yang terakhir)

Catatan yang yashmemiliki masalah serupa dengan fitur pengalihan prosesnya .cmd1 >(cmd2)akan ditulis di cmd1 /dev/fd/3 3>(cmd2)sana. Tetapi cmd2tidak menunggu dan Anda tidak dapat menggunakannya waituntuk menunggu dan pidnya tidak tersedia dalam $!variabel juga. Anda akan menggunakan pekerjaan yang sama untuk bash.

Stéphane Chazelas
sumber
Pertama, saya mencoba echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;, kemudian menyederhanakannya echo one; echo two > >(cat) | cat; echo three;dan menghasilkan nilai-nilai dalam urutan yang tepat juga. Apakah semua manipulasi deskriptor 3>&1 >&4 4>&-ini diperlukan? Juga, saya tidak mendapatkan ini >&4 4>&- kami mengarahkan stdoutke fd keempat, lalu menutup fd keempat, lalu menggunakannya lagi 4>&1. Mengapa perlu dan bagaimana cara kerjanya? Mungkin, saya harus membuat pertanyaan baru tentang topik ini?
MiniMax
1
@MiniMax, tetapi di sana, Anda memengaruhi stdout cmd1dan cmd2, intinya dengan sedikit tarian dengan deskriptor file adalah mengembalikan yang asli dan hanya menggunakan pipa tambahan untuk menunggu, bukannya juga menyalurkan output dari perintah.
Stéphane Chazelas
@MiniMax Butuh beberapa saat untuk mengerti, saya tidak mendapatkan pipa pada tingkat rendah sebelumnya. Yang paling kanan 4>&1membuat file descriptor (fd) 4 untuk daftar perintah kawat gigi luar, dan membuatnya sama dengan stdout kawat gigi luar. Kawat gigi bagian dalam memiliki stdin / stdout / stderr secara otomatis diatur untuk menghubungkan ke kawat gigi luar. Namun, 3>&1membuat fd 3 terhubung ke stdin kawat gigi luar. >&4membuat kawat gigi bagian dalam menghubungkan ke kawat gigi luar fd 4 (Yang kita buat sebelumnya). 4>&-menutup fd 4 dari kawat gigi bagian dalam (Karena stdout kawat gigi bagian dalam sudah terhubung ke kawat gigi luar fd 4).
Nicholas Pipitone
@MiniMax Bagian yang membingungkan adalah bagian kanan-ke-kiri, 4>&1dieksekusi terlebih dahulu, sebelum pengalihan lainnya, sehingga Anda tidak "menggunakan lagi 4>&1". Secara keseluruhan, kawat gigi bagian dalam mengirim data ke stdout-nya, yang ditimpa dengan apa pun fd 4 itu diberikan. Fd 4 yang diberikan kawat gigi bagian dalam, adalah kawat gigi luar 'fd 4, yang sama dengan stdout asli kawat gigi luar.
Nicholas Pipitone
Bash membuatnya terasa seperti 4>5berarti "4 pergi ke 5", tetapi benar-benar "fd 4 ditimpa dengan fd 5". Dan sebelum eksekusi, fd 0/1/2 terhubung secara otomatis (Seiring dengan fd dari shell luar), dan Anda dapat menimpa mereka seperti yang Anda inginkan. Setidaknya itulah interpretasi saya terhadap dokumentasi bash. Jika Anda memahami hal lain dari ini , lmk.
Nicholas Pipitone
4

Anda dapat menyalurkan perintah kedua ke perintah lain cat , yang akan menunggu sampai pipa inputnya tertutup. Ex:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

Singkat dan sederhana.

==========

Sesederhana tampaknya, banyak yang terjadi di belakang layar. Anda dapat mengabaikan sisa jawabannya jika Anda tidak tertarik dengan cara kerjanya.

Ketika Anda memiliki echo two > >(cat); echo three, >(cat)dipotong oleh shell interaktif, dan berjalan secara independen echo two. Dengan demikian, echo twoselesai, dan kemudian echo threedieksekusi, tetapi sebelum >(cat)selesai. Ketika bashmendapatkan data dari >(cat)saat itu tidak diharapkan (beberapa milidetik kemudian), itu memberi Anda situasi seperti cepat di mana Anda harus menekan baris baru untuk kembali ke terminal (Sama seperti jika pengguna lain mesgmenyunting Anda).

Namun, mengingat echo two > >(cat) | cat; echo three, dua subkulit muncul (sesuai dokumentasi |simbol).

Satu subkulit bernama A adalah untuk echo two > >(cat), dan satu subkulit bernama B adalah untuk cat. A secara otomatis terhubung ke B (stdout A adalah stdin B). Kemudian, echo twodan >(cat)mulai jalankan. >(cat)stdout diatur ke stdout A, yang sama dengan stdin B. Setelah echo twoselesai, A keluar, menutup stdout-nya. Namun, >(cat)masih memegang referensi ke stdin B. catStdin kedua memegang stdin B, dan itu cattidak akan keluar sampai ia melihat EOF. EOF hanya diberikan ketika tidak ada lagi file yang dibuka dalam mode tulis, jadi >(cat)stdout memblokir yang kedua cat. B tetap menunggu pada detik itu cat. Karena memerah buffer dan keluar. Tidak ada yang memegang stdin B / detik lagi,echo two keluar, >(cat)akhirnya mendapat EOF, jadi>(cat)catcatmembaca EOF (B sama sekali tidak membaca stdin, tidak peduli). EOF ini menyebabkan catbuffer kedua menyiram buffer, menutup stdout, dan keluar, dan kemudian B keluar karenacat keluar dan B sedang menunggu cat.

Peringatan dari ini adalah bahwa bash juga memunculkan subkulit untuk >(cat)! Karena ini, Anda akan melihatnya

echo two > >(sleep 5) | cat; echo three

masih akan menunggu 5 detik sebelum dieksekusi echo three, meskipun sleep 5tidak memegang stdin B. Ini karena subkulit tersembunyi yang ditumbuhkan C >(sleep 5)sedang menunggu sleep, dan C memegang stdin B. Anda bisa lihat caranya

echo two > >(exec sleep 5) | cat; echo three

Tidak akan menunggu, karena sleeptidak memegang stdin B, dan tidak ada subshell hantu C yang memegang stdin B (eksekutif akan memaksa tidur untuk menggantikan C, yang bertentangan dengan forking dan membuat C menunggu sleep). Terlepas dari peringatan ini,

echo two > >(exec cat) | cat; echo three

masih akan menjalankan fungsi dengan benar, seperti dijelaskan sebelumnya.

Nicholas Pipitone
sumber
Sebagaimana dicatat dalam konversi dengan @MiniMax di komentar untuk jawaban saya, yang memiliki kelemahan mempengaruhi stdout dari perintah dan berarti output perlu dibaca dan ditulis waktu tambahan.
Stéphane Chazelas
Penjelasannya tidak akurat. Atidak menunggu catmelahirkan di >(cat). Seperti yang saya sebutkan dalam jawaban saya, alasan mengapa echo two > >(sleep 5 &>/dev/null) | cat; echo threeoutput threesetelah 5 detik adalah karena versi saat ini bashlimbah proses shell tambahan >(sleep 5)yang menunggu sleepdan proses itu masih stdout pergi ke pipeyang mencegah yang kedua catdari penghentian. Jika Anda menggantinya dengan echo two > >(exec sleep 5 &>/dev/null) | cat; echo threeuntuk menghilangkan proses tambahan itu, Anda akan menemukan bahwa itu mengembalikan langsung.
Stéphane Chazelas
Itu membuat subshell bersarang? Saya telah mencoba untuk melihat implementasi bash untuk mengetahuinya, saya cukup yakin echo two > >(sleep 5 &>/dev/null)minimum mendapatkan subshell sendiri. Apakah ini merupakan detail implementasi yang tidak terdokumentasi yang menyebabkan sleep 5mendapatkan subkulitnya sendiri juga? Jika didokumentasikan maka itu akan menjadi cara yang sah untuk menyelesaikannya dengan karakter yang lebih sedikit (Kecuali jika ada loop ketat saya tidak berpikir ada yang akan melihat masalah kinerja dengan subkulit, atau kucing) `. Jika tidak didokumentasikan maka rip, hack yang bagus, tidak akan bekerja pada versi yang akan datang.
Nicholas Pipitone
$(...), <(...)memang melibatkan subkulit, tetapi ksh93 atau zsh akan menjalankan perintah terakhir dalam subkulit itu dalam proses yang sama, bukan bashitu sebabnya masih ada proses lain memegang pipa terbuka saat sleepsedang menjalankan tidak memegang pipa terbuka. Versi masa depan bashdapat menerapkan pengoptimalan yang serupa.
Stéphane Chazelas
1
@ StéphaneChazelas Saya memperbarui jawaban saya dan saya pikir penjelasan saat ini dari versi yang lebih pendek sudah benar, tetapi Anda tampaknya tahu detail implementasi shell sehingga Anda dapat memverifikasi. Saya pikir solusi ini harus digunakan sebagai lawan dari tarian deskriptor file meskipun, karena bahkan di bawah exec, itu berfungsi seperti yang diharapkan.
Nicholas Pipitone