Bagaimana saya bisa mencapai portabilitas dengan sed -i (in-place editing)?

41

Saya sedang menulis skrip shell untuk server saya, yang merupakan hosting bersama yang menjalankan FreeBSD. Saya juga ingin dapat mengujinya secara lokal, di PC saya yang menjalankan Linux. Oleh karena itu, saya mencoba menulisnya dengan cara yang portabel, tetapi dengan sedsaya tidak melihat cara untuk melakukannya.

Bagian dari situs web saya menggunakan file HTML statis yang dihasilkan, dan baris sed ini menyisipkan DOCTYPE yang benar setelah setiap regenerasi:

sed -i '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Ini bekerja dengan GNU seddi Linux, tetapi FreeBSD sedmengharapkan argumen pertama setelah -iopsi menjadi ekstensi untuk salinan cadangan. Ini akan terlihat seperti:

sed -i '' '1s/^/<!DOCTYPE html> \n/' ${file_name.html}

Namun, GNU sedpada gilirannya mengharapkan ekspresi mengikuti segera setelahnya -i. (Ini juga membutuhkan perbaikan dengan penanganan baris baru, tapi itu sudah dijawab di sini )

Tentu saja saya dapat memasukkan perubahan ini dalam salinan server saya dari skrip, tetapi itu akan mengacaukan yaitu penggunaan VCS untuk versi. Apakah ada cara untuk mencapai ini dengan sed dalam cara yang sepenuhnya portabel?

Merah
sumber
Dua cuplikan potongan yang Anda berikan identik, apakah Anda yakin tidak ada kesalahan ketik? Juga, saya dapat menjalankan GNU sed memasok ekstensi cadangan tepat setelah-i
iruvar
Duh, terima kasih sudah melihat ini. Saya telah memperbaiki pertanyaan saya. Baris kedua menghasilkan kesalahan pada versi saya, ia mengharapkan '1s / ^ / <! DOCTYPE html> \ n /' menjadi file dan mengeluh tidak dapat menemukannya.
Merah
1
Referensi silang: sed in-place flag yang berfungsi baik pada Mac (BSD) dan Linux pada Stack Overflow.
MvG

Jawaban:

40

GNU sed menerima ekstensi opsional setelahnya -i. Perpanjangan harus dalam argumen yang sama tanpa ruang intervensi. Sintaks ini juga berfungsi pada BSD sed.

sed -i.bak -e '…' SOMEFILE

Perhatikan bahwa pada BSD, -ijuga mengubah perilaku ketika ada beberapa file input: mereka diproses secara independen (jadi misalnya $cocok dengan baris terakhir dari setiap file). Juga ini tidak akan berfungsi pada BusyBox.

Jika Anda tidak ingin menggunakan file cadangan, Anda dapat memeriksa versi sed yang tersedia.

case $(sed --help 2>&1) in
  *GNU*) set sed -i;;
  *) set sed -i '';;
esac
"$@" -e '…' "$file"

Atau sebagai alternatif, untuk menghindari mengacaukan parameter posisi, tentukan fungsi.

case $(sed --help 2>&1) in
  *GNU*) sed_i () { sed -i "$@"; };;
  *) sed_i () { sed -i '' "$@"; };;
esac
sed_i -e '…' "$file"

Jika Anda tidak ingin repot, gunakan Perl.

perl -i -pe '…' "$file"

Jika Anda ingin menulis skrip portabel, jangan gunakan -i- ini bukan di POSIX. Lakukan secara manual apa yang dilakukan sed di bawah tenda - itu hanya satu baris kode lagi.

sed -e '…' "$file" >"$file.new"
mv -- "$file.new" "$file"
Gilles 'SANGAT berhenti menjadi jahat'
sumber
2
GNU sed -ijuga menyiratkan -s. Dan cara termudah untuk memeriksa GNU sedadalah dengan sed vperintah yang merupakan noop sah untuk GNU tetapi gagal di tempat lain.
mikeserv
Terinspirasi oleh tips di atas, berikut ini adalah versi portabel single-line (jika jelek) bagi mereka yang benar-benar menginginkannya, meskipun ia memunculkan subkulit: sed -i$(sed v < /dev/null 2> /dev/null || echo -n " ''") -e '...' "$file"Jika bukan GNU sed, ia menyisipkan spasi diikuti oleh dua tanda kutip setelah -isehingga bekerja pada BSD. GNU sed hanya mendapat -i.
Ivan X
2
@ IvanX Saya khawatir menggunakan keberadaan vperintah untuk menguji GNU sed. Bagaimana jika FreeBSD memutuskan untuk mengimplementasikannya?
Gilles 'SO- stop being evil'
@Gilles Fair point, tetapi halaman manual untuk GNU sed menggambarkan v sebagai tepat untuk tujuan itu (mendeteksi bahwa GNU sed dan bukan sesuatu yang lain), jadi orang akan berharap bahwa * BSD akan menghormatinya. Saya tidak bisa memikirkan tes lain, begitu saja, yang tidak mengambil tindakan pada GNU sed, sementara menyebabkan kesalahan pada BSD sed (atau sebaliknya), selain menggunakan -i itu sendiri, tetapi itu akan memerlukan membuat file dummy terlebih dahulu. Tes Anda untuk sed di atas OK tapi sulit untuk inline. Menghindari -saya sepenuhnya, seperti yang Anda sarankan, tentu tampak seperti taruhan teraman, tapi saya setuju dengan penggunaannya sed vkarena itulah tujuannya untuk tetap ada.
Ivan X
1
@lapo Sebuah alternatif adalah mendefinisikan suatu fungsi, lihat edit saya.
Gilles 'SO- berhenti bersikap jahat'
10

Jika Anda tidak menemukan trik untuk menjadikan sedpermainan menyenangkan, Anda dapat mencoba:

  1. Jangan gunakan -i:

    sed '1s/^/<!DOCTYPE html> \n/' "${file_name.html}" > "${file_name.html}.tmp" &&
      mv "${file_name.html}.tmp" "${file_name.html}"
    
  2. Gunakan Perl

    perl -i -pe 'print "<!DOCTYPE html> \n" if $.==1;' "${file_name.html}"
terdon
sumber
8

ed

Anda selalu dapat menggunakan eduntuk menambahkan baris ke file yang ada.

$ printf '0a\n<!DOCTYPE html>\n.\nw\n' | ed my.html

Detail

Bit di sekitar <!DOCTYPE html>adalah perintah untuk edmenginstruksikannya untuk menambahkan baris itu ke file my.html.

sed

Saya percaya perintah sedini juga dapat digunakan:

$ sed -i '1i<!DOCTYPE html>\n` testfile.csv
slm
sumber
Saya akhirnya menggunakan Perl, tetapi menggunakan ed adalah alternatif yang baik yang tidak populer di kalangan pengguna seperti Unix.
Merah
@ Merah - senang mendengar Anda menyelesaikan masalah Anda. Ya saya belum pernah melihat yang sebelumnya, googling mengubahnya dan itu benar-benar tampak seperti cara yang paling portabel dan tepat untuk melakukan ini.
slm
7

Anda juga dapat melakukan secara manual apa yang perl -iada di bawah tenda:

{ rm -f file && { echo '<!DOCTYPE html>'; cat; } > file;} < file

Seperti perl -i, tidak ada cadangan, dan seperti kebanyakan solusi yang diberikan di sini, berhati-hatilah itu dapat memengaruhi izin, kepemilikan file, dan dapat mengubah symlink menjadi file biasa.

Dengan:

sed '1i\
<!DOCTYPE html>' file 1<> file

sedakan menimpa file itu sendiri, sehingga tidak akan memengaruhi kepemilikan dan izin atau symlink. Ini berfungsi dengan GNU sedkarena sedbiasanya akan membaca buffer penuh data dari file(4k dalam kasus saya) sebelum menimpa dengan iperintah. Itu tidak akan berhasil jika file lebih dari 4k kecuali untuk fakta yang sedjuga buffer outputnya.

Pada dasarnya sedbekerja pada balok 4k untuk membaca dan menulis. Jika jalur untuk memasukkan lebih kecil dari 4k, sedtidak akan pernah menimpa blok yang belum dibaca.

Saya tidak akan mengandalkan itu.

Stéphane Chazelas
sumber
Seharusnya echo '<!DOCTYPE html>'atau melarikan diri tanpa "" kutipan.
AD
@AD Poin bagus. Saya cenderung melupakan bug itu ^ Wfature bash / zsh interaktif karena saya biasanya menonaktifkannya untuk diri saya sendiri.
Stéphane Chazelas
Ini adalah salah satu dari sangat sedikit jawaban di sini yang tidak memiliki lubang keamanan yang terbuka lebar mengarahkan ke "file temp" dengan statis, nama diprediksi tanpa memeriksa jika sudah ada. Saya percaya ini harus menjadi jawaban yang diterima. Demonstrasi yang sangat bagus dari penggunaan perintah grup, juga.
Wildcard
3

FreeBSD sed , yang digunakan pada Mac OS X juga, membutuhkan -eopsi setelah -isaklar untuk mendefinisikan & mengenali perintah (regex) berikut dengan benar & jelas.

Dengan kata lain, sed -i -e ...harus bekerja dengan FreeBSD & GNU sed.

Secara lebih umum, menghilangkan ekstensi cadangan setelah FreeBSD sed -imemerlukan beberapa sedopsi eksplisit atau beralih mengikuti -iuntuk menghindari kebingungan pada bagian FreeBSD sedsambil mem-parsing argumen command-line-nya.

(Namun, perhatikan bahwa sedpengeditan file di tempat menyebabkan perubahan inode file, lihat "Di tempat" mengedit file ).

(Sebagai petunjuk umum, FreeBSD versi terbaru sedmemiliki -rsaklar untuk meningkatkan kompatibilitas dengan GNU sed).

echo a > testfile.txt
ls -li testfile.txt
#gsed -i -e 's/a/A/' testfile.txt
#bsdsed -i 's/a/A/' testfile.txt  # does not work
bsdsed -i -e 's/a/A/' testfile.txt
ls -li testfile.txt
cat testfile.txt
carlo
sumber
5
Tidak, bsdsed -i -e 's/a/A/'ini bukan di tempat editing, itu mengedit dengan tabungan asli dengan "-e" akhiran ( testfile.txt-e).
Stéphane Chazelas
perhatikan bahwa GNU sed juga mendukung -E (selain -r) untuk kompatibilitas dengan FreeBSD. -E kemungkinan akan ditentukan dalam versi POSIX berikutnya, jadi kita semua harus melupakan itu -r tidak masuk akal dan berpura-pura itu tidak pernah ada.
Stéphane Chazelas
2

Untuk meniru sed -iuntuk satu file dengan mudah sambil menghindari kondisi balapan sebanyak mungkin:

sed 'script' <<FILE >file
$(cat file)
FILE

Omong-omong, ini juga menangani masalah yang mungkin muncul di dalamnya sed -i, tergantung pada izin direktori dan file, sed -imungkin memungkinkan pengguna untuk menimpa file yang pengguna tidak memiliki izin untuk mengedit.

Anda juga dapat melakukan pencadangan seperti:

sed 'script' <<FILE >file
$(tee file.old <file)
FILE
mikeserv
sumber
2

Anda dapat menggunakan Vim dalam mode Ex:

ex -sc '1i|<!DOCTYPE html>' -cx file
  1. 1 pilih baris pertama

  2. i masukkan teks dan baris baru

  3. x Simpan dan tutup

Atau bahkan, seperti dalam komentar, standar ex lama :

printf '%s\n' 1i '<!DOCTYPE html>' . x | ex file 
Steven Penny
sumber
Tidak ada yang spesifik-Vim di sini. Ini sepenuhnya sesuai dengan spesifikasi POSIX untukex , kecuali bahwa implementasi tidak diperlukan untuk mendukung banyak -cflag. Untuk portabilitas yang pasti saya akan gunakanprintf '%s\n' 1i '<!DOCTYPE html>' . x | ex file
Wildcard