Pengalihan IO dan perintah kepala

9

Saya mencoba mengedit .hgignorefile dengan cepat dari bash shell Cygwin hari ini, dan saya menambahkan baris yang merupakan kesalahan. Saya tidak yakin apakah ini cara terbaik untuk melakukannya, tetapi saya segera berpikir untuk menggunakan head -1 .hgignoreuntuk menghapus baris yang menyinggung (saya sebelumnya hanya memiliki satu baris dalam file). Benar saja, ketika dieksekusi itu memberikan baris pertama sebagai satu-satunya output.

Tetapi ketika saya mencoba mengarahkan output dan menulis ulang menggunakan file head -1 .hgignore > .hgignore, file itu kosong. Mengapa ini terjadi? Jika saya mencoba menambahkan head -1 .hgignore >> .hgignore, itu menambahkan dengan benar tetapi ini jelas bukan hasil yang diinginkan. Mengapa pengalihan truncating tidak berfungsi dalam kasus ini?

voithos
sumber

Jawaban:

10

Ketika shell mendapat baris perintah seperti: command > file.outshell itu sendiri membuka (dan mungkin membuat) file bernama file.out. Shell menyetel file descriptor 0 ke deskriptor file file yang didapat dari open. Begitulah cara pengalihan I / O bekerja: setiap proses tahu tentang deskriptor file 0, 1 dan 2.

Bagian tersulit dari ini adalah cara membuka file.out. Sebagian besar waktu, Anda ingin file.outdibuka untuk menulis pada offset 0 (yaitu terpotong) dan inilah yang shell lakukan untuk Anda. Itu terpotong .hgignore, membukanya untuk menulis, dup'ed yang diajukan ke 0, kemudian dieksekusi head. Clobbering file instan.

Di bash shell, Anda melakukan set noclobberuntuk mengubah perilaku ini.

Bruce Ediger
sumber
Aha, begitu. Saya memang berpikir bahwa shell memotong file sebelum menjalankan perintah, tetapi saya tidak tahu mengapa. Terima kasih untuk penjelasannya!
voithos
10

Saya pikir Bruce menjawab apa yang terjadi di sini dengan pipa shell.

Salah satu utilitas kecil favorit saya adalah spongeperintah dari moreutils . Ini memecahkan masalah ini dengan "menyerap" semua input yang tersedia sebelum membuka file output target dan menulis data. Ini memungkinkan Anda untuk menulis saluran pipa persis seperti yang Anda harapkan:

$ head -1 .hgignore | sponge .hgignore

Solusi poor-man adalah mem-pipe output ke file sementara, kemudian setelah pipline selesai (misalnya perintah berikutnya yang Anda jalankan) adalah memindahkan file temp kembali ke lokasi file asli.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Caleb
sumber
Melihat hal ini beberapa tahun kemudian, sebuah pemikiran muncul di benak saya: tidak bisakah kita lakukan saja head -1 .hgignore | tee .hgignore? teeada di coreutils, dan sebagai efek samping, ini juga menulis ke STDOUT
voithos
@voithos Setahu saya teemembuka dan memotong file yang sedang ditulisnya saat instantiated sama seperti yang lainnya sehingga tidak menyelesaikan masalah utama di sini tentang kondisi balapan pada membaca konten file sebelum Anda memotongnya dengan menulis.
Caleb
Anda memunculkan poin yang saya tidak sadari, sebenarnya - yaitu, bahwa perintah pipa dimulai segera, bukan secara berurutan. Apakah itu akurat? Namun, saya mengujinya dan tee tampaknya melakukan hal yang diinginkan. Saya punya versi 8.13di komputer saya.
voithos
1
@voithos Ya perintah dalam pipa dan semua saluran input / output yang terlibat dimulai dalam urutan terbalik sehingga pipa siap untuk menerima data ketika yang pertama mulai memberikannya. Saya menduga tes Anda cacat karena Anda mungkin menggunakan sepotong data yang terlalu kecil dan itu membuat semuanya tersimpan dalam buffer baca sebelum Anda membutuhkannya. The teeProgram akan memotong file Anda, tidak setup untuk menggandakan penyangga mereka.
Caleb
3

Di

head -n 1 file > file

fileterpotong sebelum headdimulai, tetapi jika Anda menulisnya:

head -n 1 file 1<> file

tidak seperti fileyang dibuka dalam mode baca-tulis. Namun, ketika headselesai menulis, itu tidak memotong file, jadi baris di atas akan menjadi no-op ( headhanya akan menulis ulang baris pertama di atas dirinya sendiri dan membiarkan yang lain tidak tersentuh).

Namun, setelah headkembali dan ketika fdmasih terbuka, Anda dapat memanggil perintah lain yang melakukan truncate.

Contohnya:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Yang penting di sini adalah bahwa di truncateatas, headhanya memindahkan kursor untuk fd 1 di dalam file tepat setelah baris pertama. Itu menulis ulang baris pertama yang tidak kita butuhkan, tapi itu tidak berbahaya.

Dengan kepala POSIX, kita benar-benar bisa pergi tanpa menulis ulang baris pertama itu:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Di sini, kami menggunakan fakta yang headmenggerakkan posisi kursor di stdin-nya. Walaupun headbiasanya akan membaca inputnya dengan potongan besar untuk meningkatkan kinerja, POSIX akan mengharuskannya (jika mungkin) untuk seekkembali tepat setelah baris pertama jika sudah melampaui itu. Namun perlu dicatat bahwa tidak semua implementasi melakukannya.

Atau, Anda dapat menggunakan perintah shell readsebagai gantinya dalam hal ini:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Stéphane Chazelas
sumber
1
Stephane, apakah Anda tahu perintah standar atau coreutils yang dapat memotong STDINmirip dengan apa yang telah Anda capai menggunakan di perlatas
iruvar
2
@ 1_CR, tidak. dddapat memotong pada offset absolut sewenang-wenang dalam file sekalipun. Jadi Anda dapat menentukan byte offset dari baris kedua dan memotong dari sana dengandd bs=1 seek="$offset" of=file
Stéphane Chazelas
1

Solusi Pria Sejati adalah

ed .hgignore
$d
wq

atau sebagai one-liner

printf '%s\n' '$d' 'wq' | ed .hgignore

Atau dengan sed GNU:

sed -i '$d' .hgignore

(Tidak, saya bercanda. Saya akan menggunakan editor interaktif. vi .hgignore GddZZ)

Gilles 'SANGAT berhenti menjadi jahat'
sumber
Saya bertanya-tanya, apakah ada manfaat menggunakan :wqlebih ZZ?
voithos
Juga, :xitulah yang jari saya lakukan secara otomatis
glenn jackman
dan ZQsama dengan:q!
glenn jackman
ZZ dan: x hanya menulis jika ada sesuatu untuk ditulis ...: w selalu fsyncs file ke disk terlepas dari jika diperlukan. Saya menggunakan: xa karena saya menggunakan tab.
xenoterracide
1

Anda dapat menggunakan Vim dalam mode Ex:

ex -sc '2,d|x' .hgignore
  1. 2, pilih garis 2 sampai akhir

  2. d menghapus

  3. x Simpan dan tutup

Steven Penny
sumber
0

Untuk mengedit file di tempat Anda juga dapat menggunakan trik menangani file terbuka seperti yang ditunjukkan oleh Jürgen Hötzel di Redirect output dari sed 's / c / d /' myFile ke myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
sumber
2
Dan tepat setelah rm .hgignorekekuatan Anda gagal, mengambil jam kerja keras. Oke, itu tidak penting .hgignore, tetapi mengapa Anda tetap melakukan sesuatu yang rumit? Jadi downvote saya: secara teknis benar tetapi ide yang sangat buruk.
Gilles 'SANGAT berhenti menjadi jahat'
@Gilles, mungkin ide yang tidak terlalu bagus, tapi itulah contohnya perl -i(untuk mengedit di tempat), dan saya tidak akan terkejut jika beberapa implementasi sed -imelakukannya juga (meskipun versi terbaru dari GNU sedtampaknya tidak).
Stéphane Chazelas