Bagaimana saya bisa menggunakan sed untuk mengganti string multi-line?

243

Saya perhatikan bahwa, jika saya menambahkan \npola untuk menggantikan penggunaan sed, itu tidak cocok. Contoh:

$ cat > alpha.txt
This is
a test
Please do not
be alarmed

$ sed -i'.original' 's/a test\nPlease do not/not a test\nBe/' alpha.txt

$ diff alpha.txt{,.original}

$ # No differences printed out

Bagaimana saya bisa membuatnya bekerja?

Belmin Fernandez
sumber
Solusi cerdas di sini: unix.stackexchange.com/a/445666/61742 . Tentu saja itu tidak berkinerja! Opsi bagus lainnya untuk melakukan penggantian sesuai dengan kebutuhan Anda mungkin awk, perl dan python. Ada banyak yang lain, tetapi saya percaya bahwa awk adalah yang paling universal di berbagai distribusi Linux (misalnya). Terima kasih!
Eduardo Lucio

Jawaban:

235

Dalam panggilan sed sederhana , ia memiliki satu baris teks dalam ruang pola, yaitu. 1 baris \nteks yang dibatasi dari input. Baris tunggal di ruang pola tidak memiliki \n... Itu sebabnya regex Anda tidak menemukan apa pun.

Anda dapat membaca banyak baris ke dalam pola-ruang dan memanipulasi hal-hal dengan sangat baik, tetapi dengan upaya yang lebih dari normal .. Sed memiliki seperangkat perintah yang memungkinkan jenis hal ini ... Berikut adalah tautan ke Ringkasan Perintah untuk sed . Itu yang terbaik yang saya temukan, dan membuat saya berputar.

Namun, lupakan ide "satu-liner" begitu Anda mulai menggunakan perintah-mikro sed. Sangat berguna untuk meletakkannya seperti program terstruktur sampai Anda merasakannya ... Anehnya sederhana, dan sama-sama tidak biasa. Anda bisa menganggapnya sebagai "bahasa assembler" dari pengeditan teks.

Rangkuman: Gunakan sed untuk hal-hal sederhana, dan mungkin sedikit lebih, tetapi secara umum, ketika melampaui bekerja dengan satu baris, kebanyakan orang lebih suka sesuatu yang lain ...
Saya akan membiarkan orang lain menyarankan sesuatu yang lain .. Saya benar-benar tidak yakin apa pilihan terbaik (saya akan menggunakan sed, tapi itu karena saya tidak tahu perl cukup baik.)


sed '/^a test$/{
       $!{ N        # append the next line when not on the last line
         s/^a test\nPlease do not$/not a test\nBe/
                    # now test for a successful substitution, otherwise
                    #+  unpaired "a test" lines would be mis-handled
         t sub-yes  # branch_on_substitute (goto label :sub-yes)
         :sub-not   # a label (not essential; here to self document)
                    # if no substituion, print only the first line
         P          # pattern_first_line_print
         D          # pattern_ltrunc(line+nl)_top/cycle
         :sub-yes   # a label (the goto target of the 't' branch)
                    # fall through to final auto-pattern_print (2 lines)
       }    
     }' alpha.txt  

Ini skrip yang sama, diringkas menjadi apa yang jelas lebih sulit untuk dibaca dan bekerja dengan, tetapi beberapa orang akan dengan ragu menyebut satu kalimat

sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;ty;P;D;:y}}' alpha.txt

Ini perintah saya "cheat-sheet"

:  # label
=  # line_number
a  # append_text_to_stdout_after_flush
b  # branch_unconditional             
c  # range_change                     
d  # pattern_delete_top/cycle          
D  # pattern_ltrunc(line+nl)_top/cycle 
g  # pattern=hold                      
G  # pattern+=nl+hold                  
h  # hold=pattern                      
H  # hold+=nl+pattern                  
i  # insert_text_to_stdout_now         
l  # pattern_list                       
n  # pattern_flush=nextline_continue   
N  # pattern+=nl+nextline              
p  # pattern_print                     
P  # pattern_first_line_print          
q  # flush_quit                        
r  # append_file_to_stdout_after_flush 
s  # substitute                                          
t  # branch_on_substitute              
w  # append_pattern_to_file_now         
x  # swap_pattern_and_hold             
y  # transform_chars                   
Peter.O
sumber
167
Tembak aku sekarang. Sintaks terburuk yang pernah ada!
Gili
53
Ini adalah penjelasan yang fantastis, tetapi saya cenderung setuju dengan @Gili.
gatoatigrado
11
Cheat-sheet Anda memiliki semuanya.
konsolebox
3
Anda tidak perlu label untuk menggunakan tperintah di sini — ketika tidak diberi label, default untuk percabangan di akhir skrip. Begitu sed '/^a test$/{$!{N;s/^a test\nPlease do not$/not a test\nBe/;t;P;D}}' alpha.txtjuga persis sama dengan perintah Anda dalam segala situasi. Tentu saja untuk file khusus ini , sed '/test/{N;s/.*/not a test\nBe/}' alpha.txtmelakukan hal yang sama juga, tetapi contoh pertama saya secara logis setara untuk semua file yang mungkin. Perhatikan juga bahwa \ndalam string pengganti tidak menghasilkan baris baru; Anda memerlukan garis miring terbalik `\` diikuti oleh baris baru yang sebenarnya untuk melakukan itu.
Wildcard
9
Perhatikan bahwa sintaks itu spesifik untuk GNU ( #perintah tidak terpisahkan dari yang sebelumnya, \ndalam RHS of s). Dengan GNU sedAnda juga dapat menggunakan -zuntuk menggunakan catatan dibatasi NUL (dan kemudian menghirup seluruh input jika itu teks (yang menurut definisi tidak mengandung NUL)).
Stéphane Chazelas
181

Gunakan perlalih-alih sed:

$ perl -0777 -i.original -pe 's/a test\nPlease do not/not a test\nBe/igs' alpha.txt
$ diff alpha.txt{,.original}
2,3c2,3
< not a test
< Be
---
> a test
> Please do not

-pi -eadalah urutan baris perintah "ganti di tempat" standar Anda, dan -0777 menyebabkan perl untuk menyeruput seluruh file. Lihat perlrun perldoc untuk mencari tahu lebih banyak tentang hal itu.

kepala kode
sumber
3
Terima kasih! Untuk pekerjaan multiline, perl menang dengan mudah! Saya akhirnya menggunakan `$ perl -pi -e 's / bar / baz /' fileA` untuk mengubah file di tempat.
Nicholas Tolley Cottrell
3
Sangat umum bahwa poster asli meminta seddan menjawab menggunakan awk atau perl muncul. Saya pikir itu bukan pada topik, karenanya, maaf, tapi saya memecat satu minus.
Rho Phi
68
+1 & tidak setuju dengan Roberto. Seringkali pertanyaan diungkapkan secara khusus untuk ketidaktahuan metode yang lebih baik. Ketika tidak ada perbedaan kontekstual substantif (seperti di sini), solusi optimal harus mendapatkan setidaknya profil sebanyak yang spesifik pertanyaan.
geoteori
56
Saya pikir sedjawaban di atas membuktikan bahwa jawaban Perl ada pada topik.
reinierpost
7
Sedikit lebih mudah: Dengan "-p0e" "-0777" tidak diperlukan. unix.stackexchange.com/a/181215/197502
Weidenrinde
96

Saya pikir, lebih baik mengganti \nsimbol dengan simbol lain, dan kemudian bekerja seperti biasa:

mis. kode sumber tidak berfungsi:

cat alpha.txt | sed -e 's/a test\nPlease do not/not a test\nBe/'

dapat diubah menjadi:

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test\rPlease do not/not a test\rBe/'  | tr '\r' '\n'

Jika ada yang tidak tahu, \napakah UNIX mengakhiri baris, \r\n- windows, \r- Mac OS klasik. Teks UNIX normal tidak menggunakan \rsimbol, jadi aman digunakan untuk kasus ini.

Anda juga dapat menggunakan beberapa simbol eksotis untuk mengganti \ n sementara. Sebagai contoh - \ f (simbol umpan formulir). Anda dapat menemukan lebih banyak simbol di sini .

cat alpha.txt | tr '\n' '\f' | sed -e 's/a test\fPlease do not/not a test\fBe/'  | tr '\f' '\n'
xara
sumber
11
+1 untuk peretasan pintar ini! Yang sangat berguna adalah saran tentang penggunaan simbol eksotis untuk menggantikan sementara baris baru kecuali jika Anda benar-benar yakin tentang konten file yang sedang Anda edit.
L0j1k
Ini tidak berfungsi seperti yang ditulis pada OS X. Sebaliknya, kita perlu mengganti semua instance \rdalam argumen seddengan $(printf '\r').
abeboparebop
@abeboparebop: great ditemukan! 👍 Atau, instal GNU sed menggunakan homebrew: stackoverflow.com/a/30005262
ssc
@ abeboparebop, Pada OSX, Anda hanya perlu menambahkan $sebelum string sed untuk mencegahnya mengubah \rmenjadi r. Contoh singkat: sed $'s/\r/~/'. Contoh lengkap:cat alpha.txt | tr '\n' '\r' | sed $'s/a test\rPlease do not/not a test\rBe/' | tr '\r' '\n'
wisbucky
40

Semua hal dipertimbangkan, melahap seluruh file mungkin cara tercepat untuk pergi.

Sintaks dasar adalah sebagai berikut:

sed -e '1h;2,$H;$!d;g' -e 's/__YOUR_REGEX_GOES_HERE__...'

Pikiran Anda, melahap seluruh file mungkin tidak menjadi pilihan jika file tersebut sangat besar. Untuk kasus seperti itu, jawaban lain yang disediakan di sini menawarkan solusi khusus yang dijamin dapat bekerja pada jejak memori kecil.

Untuk semua situasi hack dan slash lainnya, cukup dengan menambahkan -e '1h;2,$H;$!d;g'diikuti oleh sedargumen regex asli Anda cukup banyak menyelesaikan pekerjaan.

misalnya

$ echo -e "Dog\nFox\nCat\nSnake\n" | sed -e '1h;2,$H;$!d;g' -re 's/([^\n]*)\n([^\n]*)\n/Quick \2\nLazy \1\n/g'
Quick Fox
Lazy Dog
Quick Snake
Lazy Cat

Apa yang -e '1h;2,$H;$!d;g'harus dilakukan

The 1, 2,$, $!bagian adalah garis penentu yang batas yang garis perintah langsung berikut berjalan pada.

  • 1: Baris pertama saja
  • 2,$: Semua baris mulai dari yang kedua
  • $!: Setiap baris selain yang terakhir

Jadi diperluas, inilah yang terjadi pada setiap baris dari input jalur N.

  1: h, d
  2: H, d
  3: H, d
  .
  .
N-2: H, d
N-1: H, d
  N: H, g

The gperintah tidak diberi specifier line, tetapi sebelumnya dperintah memiliki klausul khusus " Mulai siklus berikutnya. ", Dan ini mencegah gdari berjalan pada semua lini kecuali yang terakhir.

Adapun arti dari setiap perintah:

  • Pertama hdiikuti oleh Hs pada setiap salinan baris kata baris masukan ke dalam sed's ruang ditahan . (Pikirkan buffer teks sewenang-wenang.)
  • Setelah itu, dbuang setiap baris untuk mencegah agar baris-baris ini tidak ditulis ke output. The ruang hold namun yang diawetkan.
  • Akhirnya, pada baris terakhir, gmengembalikan akumulasi setiap baris dari ruang tunggu sehingga sedmampu menjalankan regexnya pada seluruh input (daripada secara garis-pada-waktu), dan karenanya mampu cocok pada \ns.
antak
sumber
38

sedmemiliki tiga perintah untuk mengelola operasi multi-line: N, Ddan P(membandingkannya dengan yang normal n , ddan p).

Dalam hal ini, Anda dapat mencocokkan baris pertama dari pola Anda, gunakan Nuntuk menambahkan baris kedua ke ruang pola dan kemudian gunakan suntuk melakukan substitusi Anda.

Sesuatu seperti:

/a test$/{
  N
  s/a test\nPlease do not/not a test\nBe/
}
andcoz
sumber
2
Ini luar biasa! Lebih sederhana dari pada jawaban yang diterima dan masih efektif.
jeyk
Dan semua yang melibatkan ruang hold ( G, H, x...). Lebih banyak garis dapat ditambahkan ke dalam ruang pola dengan sperintah juga.
Stéphane Chazelas
solusi ini tidak bekerja dengan kasus berikut "Ini adalah \ na test \ na test \ n Harap jangan \ n waspada"
mug896
@ mug896 Anda kemungkinan besar membutuhkan banyak Nperintah
loa_in_
15

Anda bisa tetapi sulit . Saya sarankan beralih ke alat lain. Jika ada ekspresi reguler yang tidak pernah cocok dengan bagian mana pun dari teks yang ingin Anda ganti, Anda dapat menggunakannya sebagai pemisah rekaman awk di GNU awk.

awk -v RS='a' '{gsub(/hello/, "world"); print}'

Jika tidak pernah ada dua baris baru berturut-turut dalam string pencarian Anda, Anda dapat menggunakan "mode paragraf" awk (satu atau lebih baris kosong memisahkan catatan).

awk -v RS='' '{gsub(/hello/, "world"); print}'

Solusi mudah adalah dengan menggunakan Perl dan memuat file sepenuhnya ke memori.

perl -0777 -pe 's/hello/world/g'
Gilles
sumber
1
Bagaimana cara menerapkan perintah perl ke file?
sebix
2
@sebix perl -0777 -pe '…' <input-file >output-file. Untuk memodifikasi file di tempat,perl -0777 -i -pe '…' filename
Gilles
3
Lihat juga GNU sed's -zpilihan (ditambahkan pada 2012 setelah jawaban yang telah diposting): seq 10 | sed -z 's/4\n5/a\nb/'.
Stéphane Chazelas
7

Saya pikir ini adalah solusi sed untuk pencocokan 2 baris.

sed -n '$!N;s@a test\nPlease do not@not a test\nBe@;P;D' alpha.txt

Jika Anda ingin 3 baris yang cocok maka ...

sed -n '1{$!N};$!N;s@aaa\nbbb\nccc@xxx\nyyy\nzzz@;P;D'

Jika Anda ingin 4 baris yang cocok maka ...

sed -n '1{$!N;$!N};$!N;s@ ... @ ... @;P;D'

Jika penggantian bagian dalam perintah "s" menyusutkan garis maka sedikit lebih rumit seperti ini

# aaa\nbbb\nccc shrink to one line "xxx"

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@xxx@;$!N;$!N};P;D'

Jika bagian penggantian menumbuhkan garis maka sedikit lebih rumit seperti ini

# aaa\nbbb\nccc grow to five lines vvv\nwww\nxxx\nyyy\nzzz

sed -n '1{$!N};$!N;/aaa\nbbb\nccc/{s@@vvv\nwww\nxxx\nyyy\nzzz@;P;s/.*\n//M;P;s/.*\n//M};P;D'
mug896
sumber
Ini harus menuju ke puncak! Saya hanya menggunakan "-i" bukan "-n" untuk substitusi dua baris, karena itulah yang saya butuhkan, dan kebetulan, itu juga dalam contoh penanya.
Nagev
5
sed -i'.original' '/a test/,/Please do not/c not a test \nBe' alpha.txt

Di sini /a test/,/Please do not/dianggap sebagai blok teks (multi baris), cadalah perintah perubahan diikuti oleh teks barunot a test \nBe

Dalam hal teks yang akan diganti sangat panjang, saya akan menyarankan sintaks ex .

gibies
sumber
oops masalahnya adalah bahwa sed akan mengganti semua teks akhirnya antara / a test / dan / Tolong jangan / juga ... :(
noonex
4
sed -e'$!N;s/^\(a test\n\)Please do not be$/not \1Be/;P;D' <in >out

Hanya perlu sedikit memperluas jendela Anda pada input.

Cukup mudah. Selain substitusi standar; Anda hanya perlu $!N, Pdan Ddi sini.

mikeserv
sumber
4

Selain Perl, pendekatan umum dan praktis untuk mengedit multiline untuk stream (dan file juga) adalah:

Pertama, buat beberapa pemisah baris UNIK yang Anda inginkan, misalnya

$ S=__ABC__                     # simple
$ S=__$RANDOM$RANDOM$RANDOM__   # better
$ S=$(openssl rand -hex 16)     # ultimate

Kemudian dalam perintah sed Anda (atau alat lain), Anda ganti \ n dengan $ {S}, seperti

$ cat file.txt | awk 1 ORS=$S |  sed -e "s/a test${S}Please do not/not a test\nBe/" | awk 1 RS=$S > file_new.txt

(awk menggantikan pemisah jalur ASCII dengan milik Anda dan sebaliknya.)

tamu
sumber
2

Ini adalah modifikasi kecil dari jawaban pintar xara untuk membuatnya bekerja pada OS X (Saya menggunakan 10.10):

cat alpha.txt | tr '\n' '\r' | sed -e 's/a test$(printf '\r')Please do not/not a test$(printf '\r')Be/'  | tr '\r' '\n'

Alih-alih menggunakan secara eksplisit \r, Anda harus menggunakan $(printf '\r').

abeboparebop
sumber
1
Saat printf '\r'(atau echo -e '\r') berfungsi dengan baik, harap perhatikan bahwa Anda bisa menggunakan sintaks shell $'\r'untuk merujuk pada literal yang lolos. Misalnya, echo hi$'\n'thereakan menggemakan baris baru antara hidan there. Demikian pula, Anda dapat membungkus seluruh string sehingga setiap backslash \ akan lolos dari karakter berikutnya:echo $'hi\nthere'
Dejay Clayton
1

Saya ingin menambahkan beberapa baris HTML ke file menggunakan sed, (dan berakhir di sini). Biasanya saya hanya menggunakan perl, tetapi saya berada di kotak yang memiliki sed, bash dan tidak banyak lagi. Saya menemukan bahwa jika saya mengubah string menjadi satu baris dan biarkan bash / sed menginterpolasi semuanya \ t \ n berhasil:

HTML_FILE='a.html' #contains an anchor in the form <a name="nchor" />
BASH_STRING_A='apples'
BASH_STRING_B='bananas'
INSERT="\t<li>$BASH_STRING_A<\/li>\n\t<li>$BASH_STRING_B<\/li>\n<a name=\"nchor\"\/>"
sed -i "s/<a name=\"nchor"\/>/$INSERT/" $HTML_FILE

Akan lebih bersih memiliki fungsi untuk menghindari tanda kutip ganda dan garis miring, tetapi terkadang abstraksi adalah pencuri waktu.

Alexx Roche
sumber
1

GNU sedmemiliki -zopsi yang memungkinkan untuk menggunakan sintaks yang coba diterapkan OP. ( halaman manual )

Contoh:

$ cat alpha.txt
This is
a test
Please do not
be alarmed
$ sed -z 's/a test\nPlease do not\nbe/not a test\nBe/' -i alpha.txt
$ cat alpha.txt
This is
not a test
Be alarmed

Waspada: Jika Anda menggunakan ^dan $mereka sekarang cocok dengan awal dan akhir garis dibatasi dengan karakter NUL (tidak \n). Dan, untuk memastikan kecocokan pada semua \nbaris ( -pisah) Anda diganti, jangan lupa untuk menggunakan gbendera untuk pergantian global (mis s/.../.../g.).


Penghargaan: @ stéphane-chazelas pertama kali disebutkan -z dalam komentar di atas.

Peterino
sumber
0

Sed memecah input pada baris baru. Itu membuat hanya satu baris per loop.
Oleh karena itu tidak ada cara untuk mencocokkan \n(baris baru) jika ruang pola tidak mengandungnya.

Ada cara, meskipun, Anda dapat membuat sed menjaga dua garis berturut-turut dalam ruang pola dengan menggunakan loop:

sed 'N;l;P;D' alpha.txt

Tambahkan setiap pemrosesan yang dibutuhkan antara N dan P (menggantikan l).

Dalam hal ini (2 baris):

$ sed 'N;s/a test\nPlease do not/not a test\nBe/;P;D' alpha.txt
This is
not a test
Be
be alarmed

Atau, untuk tiga baris:

$ sed -n '1{$!N};$!N;s@a test\nPlease do not\nbe@not a test\nDo\nBe@;P;D' alpha.txt 
This is
not a test
Do
Be alarmed

Itu dengan asumsi jumlah baris yang sama akan diganti.

Ishak
sumber