Bagaimana cara menyimpan / dev / stdout lokasi target dalam skrip bash?

12

Saya memiliki skrip bash tertentu, yang ingin mempertahankan /dev/stdoutlokasi asli sebelum mengganti deskriptor file pertama dengan lokasi lain.

Jadi, secara alami, saya menulis sesuatu seperti

old_stdout=$(readlink -f /dev/stdout)

Dan itu tidak berhasil. Sangat cepat saya mengerti apa masalahnya:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Obvioulsly, $()berjalan dalam subkulit, yang disalurkan ke shell induk.

Jadi pertanyaannya adalah: adakah cara yang dapat diandalkan (mencakup portabilitas antara distribusi Linux) untuk menyimpan /dev/stdoutlokasi sebagai string dalam skrip bash?

alexey.e.egorov
sumber
Ini terdengar sedikit seperti masalah XY . Apa masalah mendasarnya?
Kusalananda
Masalah yang mendasarinya adalah skrip instalasi tertentu yang berjalan dalam dua mode - silent, di mana ia mencatat semua output ke file dan verbose, di mana ia tidak hanya log ke file, tetapi juga mencetak semuanya ke terminal. Tetapi dalam kedua mode skrip ingin berinteraksi dengan pengguna, yaitu mencetak ke terminal dan membaca respons pengguna. Jadi saya berpikir bahwa menabung /dev/stdoutakan menyelesaikan masalah dengan mencetak pesan dalam mode senyap. Alternatif mengarahkan ulang setiap tindakan lain yang menghasilkan output, dan ada cukup banyak dari mereka. Sekitar 100 kali lebih banyak dari pesan interaksi pengguna.
alexey.e.egorov
Cara standar berinteraksi dengan pengguna adalah dengan mencetak stderr. Ini, misalnya, mengapa prompt akan stderrsecara default.
Kusalananda
Sayangnya, stderrharus juga diarahkan dan disimpan, karena skrip memanggil sejumlah program eksternal, dan semua pesan kesalahan yang mungkin harus dikumpulkan dan dicatat.
alexey.e.egorov

Jawaban:

14

Untuk menyimpan deskriptor file, Anda menduplikatnya di fd lain. Menyimpan jalur ke file yang sesuai tidak cukup, Anda harus menyimpan mode pembukaan, bendera pembuka, posisi saat ini di dalam file dan sebagainya. Dan tentu saja, untuk pipa anonim, atau soket, itu tidak akan berfungsi karena mereka tidak memiliki jalur. Yang ingin Anda simpan adalah deskripsi file terbuka yang mengacu pada fd, dan menduplikasi fd sebenarnya mengembalikan fd baru ke deskripsi file terbuka yang sama .

Untuk menggandakan deskriptor file ke file lain, dengan shell seperti Bourne, sintaksnya adalah:

exec 3>&1

Di atas, fd 1 diduplikasi ke fd 3.

Apa pun fd 3 yang sudah dibuka sebelumnya akan ditutup, tetapi perhatikan bahwa fds 3 hingga 9 (biasanya lebih, hingga 99 dengan yash) dicadangkan untuk tujuan itu (dan tidak memiliki arti khusus yang bertentangan dengan 0, 1, atau 2), shell tahu untuk tidak menggunakannya untuk bisnis internalnya sendiri. Satu-satunya alasan fd 3 akan dibuka sebelumnya adalah karena Anda melakukannya di skrip 1 , atau dibocorkan oleh penelepon.

Kemudian, Anda dapat mengubah stdout menjadi sesuatu yang lain:

exec > /dev/null

Dan nanti, untuk mengembalikan stdout:

exec >&3 3>&-

( 3>&-sedang untuk menutup file descriptor yang tidak lagi kita perlukan).

Sekarang, masalah dengan itu adalah bahwa kecuali dalam ksh, setiap perintah yang Anda jalankan setelah itu exec 3>&1akan mewarisi fd 3. Itu kebocoran fd. Umumnya bukan masalah besar, tapi itu bisa menimbulkan masalah.

kshmengatur flag close-on-exec pada fds tersebut (untuk fds lebih dari 2), tetapi tidak pada shell lain dan shell lain tidak memiliki cara untuk mengatur flag itu secara manual.

Pekerjaan sekitar untuk shell lain adalah untuk menutup fd 3 untuk setiap perintah, seperti:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Tidak praktis. Di sini, cara terbaik adalah tidak menggunakan execsama sekali, tetapi mengarahkan ulang grup perintah:

{
  ls
  uname
} > file.log

Di sana, itu shell yang berhati-hati untuk menyimpan stdout dan mengembalikannya setelah itu (dan ia melakukannya secara internal dengan menduplikasi pada fd (di atas 9, di atas 99 untuk yash) dengan set flag close-on-exec ).

Catatan 1

Sekarang, manajemen fds 3 hingga 9 dapat menjadi rumit dan bermasalah jika Anda menggunakannya secara luas atau dalam fungsi, terutama jika skrip Anda menggunakan beberapa kode pihak ketiga yang pada gilirannya dapat menggunakan fds tersebut.

Beberapa kerang ( zsh, bash, ksh93, semua ditambahkan fitur ( disarankan oleh Oliver Kiddle darizsh ) sekitar waktu yang sama pada tahun 2005 setelah itu dibahas antara para pengembang mereka) memiliki sintaks alternatif untuk menetapkan fd bebas pertama di atas 10 bukan yang membantu dalam hal ini:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}
Stéphane Chazelas
sumber
Juga, kode Anda salah dalam arti bahwa fd 3 mungkin sudah diambil, seperti yang terjadi ketika skrip dijalankan dari rc.locallayanan, misalnya Jadi Anda benar-benar harus menggunakan sesuatu seperti exec {FD}>&1atau sesuatu. Tapi ini hanya didukung di bash 4, yang sangat menyedihkan. Jadi ini tidak benar-benar portabel.
alexey.e.egorov
@ alexey.e.egorov, lihat edit.
Stéphane Chazelas
Bash 3. * tidak mendukung fitur ini, dan versi ini digunakan dalam Centos 5, yang masih didukung dan masih digunakan. Dan menemukan deskriptor gratis dan kemudian eval "exec $i>&1"adalah hal yang ingin saya hindari, karena tidak praktis. Bisakah saya benar-benar mengandalkan bahwa fds di atas 9 akan gratis?
alexey.e.egorov
@ alexey.e.egorov, tidak, Anda melihatnya mundur. fds 3 hingga 9 bebas untuk digunakan (dan terserah Anda untuk mengelolanya seperti yang Anda inginkan) dan dimaksudkan untuk tujuan itu. fds di atas 9 dapat digunakan oleh shell secara internal dan menutupnya dapat memiliki konsekuensi buruk. Kebanyakan kerang tidak akan membiarkan Anda menggunakannya. bashakan membiarkan Anda menembak diri sendiri di kaki.
Stéphane Chazelas
2
@ alexey.e.egorov, jika saat mulai Anda skrip memiliki beberapa fds di (3..9) terbuka, itu karena penelepon Anda lupa untuk menutupnya atau mengatur bendera close-on-exec pada mereka. Itulah yang saya sebut kebocoran. Sekarang, mungkin si penelepon bermaksud memberikan fds itu kepada Anda, sehingga Anda dapat membaca dan / atau menulis data dari / kepada mereka, tetapi kemudian Anda akan mengetahuinya. Jika Anda tidak tahu tentang mereka, maka Anda tidak peduli, maka Anda dapat menutupnya dengan bebas (perhatikan bahwa itu hanya menutup proses skrip Anda fd, bukan dari pemanggil Anda).
Stéphane Chazelas
3

Seperti yang Anda lihat, skrip bash tidak seperti bahasa pemrograman biasa tempat Anda dapat menetapkan deskriptor file.

Solusi paling sederhana adalah dengan menggunakan sub-shell untuk menjalankan apa yang Anda inginkan diarahkan sehingga pemrosesan dapat dikembalikan ke atas-shell yang memiliki standar I / O.

Solusi alternatif akan digunakan ttyuntuk mengidentifikasi perangkat TTY dan mengontrol I / O dalam skrip Anda. Sebagai contoh:

dev=$(tty)

dan kemudian kamu bisa ..

echo message > $dev
Julie Pelletier
sumber
> Solusi alternatif adalah menggunakan tty untuk mengidentifikasi perangkat TTY dan mengontrol I / O dalam skrip Anda. Bagaimana caranya?
alexey.e.egorov
1
Saya baru saja memasukkan contoh dalam jawaban saya.
Julie Pelletier
1

$$ akan memberi Anda proses PID saat ini, dalam hal shell interaktif atau skrip PID shell yang relevan.

Jadi Anda bisa menggunakan:

readlink -f /proc/$$/fd/1

Contoh:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33
heemayl
sumber
1
Meskipun fungsional, bergantung pada /procstruktur tertentu menyebabkan masalah portabilitas, seperti halnya menggunakan /dev/stdoutseperti yang disebutkan dalam pertanyaan.
Julie Pelletier
1
@JuliePelletier Mengandalkan /procstruktur tertentu? Ini akan bekerja pada Linux yang memiliki procfs..
heemayl
1
Benar, sehingga kita dapat menggeneralisasi untuk Linux karena procfshampir selalu ada, tetapi kita sering melihat pertanyaan portabilitas dan metodologi pengembangan yang baik termasuk mempertimbangkan portabilitas ke sistem lain. bashdapat berjalan pada banyak sistem operasi.
Julie Pelletier