Pertandingan non-serakah dengan SED regex (meniru perl. *?)

22

Saya ingin menggunakan seduntuk menggantikan apa pun di string antara yang pertama ABdan pertama terjadinya AC(inklusif) dengan XXX.

Sebagai contoh , saya memiliki string ini (string ini adalah untuk tes saja):

ssABteAstACABnnACss

dan saya ingin output seperti ini: ssXXXABnnACss.


Saya melakukan ini dengan perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

tapi saya ingin mengimplementasikannya dengan sed. Berikut ini (menggunakan regex yang kompatibel dengan Perl) tidak berfungsi:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
sumber
2
Ini tidak masuk akal. Anda memiliki solusi yang berfungsi di Perl, tetapi Anda ingin menggunakan Sed, mengapa?
Kusalananda

Jawaban:

16

Sed regex cocok dengan pertandingan terlama. Sed tidak memiliki padanan yang tidak serakah.

Jelas yang ingin kami lakukan adalah mencocokkan

  1. AB,
    diikuti oleh
  2. jumlah apa pun selain AC,
    diikuti oleh
  3. AC

Sayangnya, sedtidak dapat melakukan # 2 - setidaknya tidak untuk ekspresi reguler multi-karakter. Tentu saja, untuk ekspresi reguler karakter tunggal seperti @(atau bahkan [123]), kita dapat melakukan [^@]*atau [^123]*. Dan agar kita dapat mengatasi keterbatasan sed dengan mengubah semua kejadian ACmenjadi @dan kemudian mencari

  1. AB,
    diikuti oleh
  2. sejumlah apa pun selain @,
    diikuti oleh
  3. @

seperti ini:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

Bagian terakhir mengubah contoh @kembali tak tertandingi AC.

Tapi, tentu saja, ini adalah pendekatan yang ceroboh, karena input sudah bisa mengandung @karakter, jadi, dengan mencocokkannya, kita bisa mendapatkan hasil positif palsu. Namun, karena tidak ada variabel shell yang akan memiliki karakter NUL ( \x00) di dalamnya, NUL kemungkinan merupakan karakter yang baik untuk digunakan dalam work-around di atas daripada @:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

Penggunaan NUL membutuhkan GNU sed. (Untuk memastikan bahwa fitur GNU diaktifkan, pengguna tidak boleh mengatur variabel shell POSIXLY_CORRECT.)

Jika Anda menggunakan sed dengan -zbendera GNU untuk menangani input yang dipisahkan NUL, seperti output dari find ... -print0, maka NUL tidak akan berada dalam ruang pola dan NUL adalah pilihan yang baik untuk substitusi di sini.

Meskipun NUL tidak bisa dalam variabel bash, dimungkinkan untuk memasukkannya dalam a printf perintah. Jika string input Anda dapat berisi karakter apa pun, termasuk NUL, maka lihat jawaban Stéphane Chazelas yang menambahkan metode pelolosan yang pintar.

John1024
sumber
Saya baru saja mengedit jawaban Anda untuk menambahkan penjelasan yang panjang; merasa bebas untuk memotongnya atau memutar kembali.
G-Man Mengatakan 'Reinstate Monica'
@ G-Man Itu penjelasan yang bagus! Dilakukan dengan sangat baik. Terima kasih.
John1024
Anda dapat echoatau printf`\ 000 'baik-baik saja di bash (atau input bisa berasal dari file). Tetapi secara umum, serangkaian teks tentu saja tidak mungkin memiliki NUL.
ilkkachu
@ilkkachu Anda benar tentang itu. Apa yang seharusnya saya tulis adalah bahwa tidak ada variabel atau parameter shell dapat berisi NUL. Jawaban diperbarui.
John1024
Bukankah ini akan jauh lebih aman jika Anda berubah ACke AC@dan kembali lagi?
Michael Vehrs
7

Beberapa sedimplementasi memiliki dukungan untuk itu. ssedmemiliki mode PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT&T as sed memiliki konjungsi dan negasi ketika menggunakan augmented regexps :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

Portable, Anda dapat menggunakan teknik ini: ganti string akhir (di sini AC) dengan satu karakter yang tidak muncul di string awal atau akhir (seperti di :sini) sehingga Anda dapat melakukannya s/AB[^:]*://, dan jika karakter tersebut dapat muncul di input , gunakan mekanisme melarikan diri yang tidak berbenturan dengan string awal dan akhir.

Sebuah contoh:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Dengan GNU sed, pendekatannya adalah menggunakan baris baru sebagai karakter pengganti. Karenased memproses satu baris pada satu waktu, baris baru tidak pernah muncul dalam ruang pola, sehingga orang dapat melakukannya:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Itu umumnya tidak bekerja dengan sedimplementasi lain karena mereka tidak mendukung [^\n]. Dengan GNU sedAnda harus memastikan bahwa kompatibilitas POSIX tidak diaktifkan (seperti dengan variabel lingkungan POSIXLY_CORRECT).

Stéphane Chazelas
sumber
6

Tidak, karena regex tidak memiliki kecocokan yang tidak serakah.

Anda dapat mencocokkan semua teks hingga kemunculan pertama ACdengan menggunakan "apa pun yang tidak mengandung AC" diikuti oleh AC, yang melakukan hal yang sama dengan Perl .*?AC. Masalahnya adalah, "apa pun yang tidak mengandung AC" tidak dapat diekspresikan dengan mudah sebagai ekspresi reguler: selalu ada ekspresi reguler yang mengenali negasi dari ekspresi reguler, tetapi regasi negasi menjadi rumit dengan cepat. Dan dalam sed portabel, ini tidak mungkin sama sekali, karena regasi negasi memerlukan pengelompokan pergantian yang hadir dalam ekspresi reguler yang diperluas (misalnya dalam awk) tetapi tidak dalam ekspresi reguler dasar portabel. Beberapa versi sed, seperti GNU sed, memang memiliki ekstensi untuk BRE yang membuatnya mampu mengekspresikan semua ekspresi reguler yang mungkin.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

Karena kesulitan meniadakan regex, ini tidak bisa digeneralisasi dengan baik. Yang bisa Anda lakukan adalah mengubah garis sementara. Dalam beberapa implementasi sed, Anda dapat menggunakan baris baru sebagai penanda, karena mereka tidak dapat muncul di baris input (dan jika Anda membutuhkan banyak penanda, gunakan baris baru diikuti oleh karakter yang berbeda-beda).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

Namun, berhati-hatilah bahwa backslash-newline tidak berfungsi di set karakter dengan beberapa versi sed. Secara khusus, ini tidak berfungsi di GNU sed, yang merupakan implementasi sed pada Linux yang tidak tertanam; di GNU sed Anda bisa menggunakan \n:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

Dalam kasus khusus ini, cukup untuk mengganti yang pertama ACdengan baris baru. Pendekatan yang saya sampaikan di atas lebih umum.

Pendekatan yang lebih kuat dalam sed adalah untuk menyimpan garis ke ruang penahanan, menghapus semua kecuali bagian "menarik" pertama dari garis, menukar ruang penahanan dan ruang pola atau menambahkan ruang pola ke ruang penahan dan ulangi. Namun, jika Anda mulai melakukan hal-hal yang rumit ini, Anda harus benar-benar berpikir untuk beralih ke awk. Awk juga tidak memiliki kecocokan non-serakah, tetapi Anda dapat membagi string dan menyimpan bagian-bagian ke dalam variabel.

Gilles 'SO- berhenti menjadi jahat'
sumber
@ilkkachu Tidak, tidak. s/\n//gmenghapus semua baris baru.
Gilles 'SANGAT berhenti menjadi jahat'
asdf. Benar, salahku.
ilkkachu
3

pencocokan sed - non serakah oleh Christoph Sieghart

Trik untuk mendapatkan pencocokan tidak serakah di sed adalah untuk mencocokkan semua karakter tidak termasuk yang mengakhiri pertandingan. Saya tahu, seorang yang tidak punya otak, tetapi saya menghabiskan menit-menit berharga untuk itu dan skrip shell seharusnya, cepat dan mudah. Jadi kalau-kalau ada orang lain yang membutuhkannya:

Pencocokan serakah

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Pencocokan non serakah

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

gresolio
sumber
3
Istilah "no-brainer" bersifat ambigu. Dalam hal ini, tidak jelas apakah Anda (atau Christoph Sieghart) memikirkan hal ini. Secara khusus, alangkah baiknya jika Anda telah menunjukkan bagaimana menyelesaikan masalah khusus dalam pertanyaan (di mana nol-lebih-dari-ekspresi diikuti oleh lebih dari satu karakter ) . Anda mungkin menemukan bahwa jawaban ini tidak berfungsi dengan baik dalam kasus itu.
Scott
Sekilas tentang lubang kelinci jauh lebih dalam daripada yang terlihat bagi saya. Anda benar, solusi yang tidak berfungsi dengan baik untuk ekspresi reguler multi-karakter.
gresolio
0

Dalam kasus Anda, Anda bisa meniadakan penutupan char dengan cara ini:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
midori
sumber
2
Pertanyaannya mengatakan, "Saya ingin mengganti apa pun antara kejadian pertama ABdan pertama ACdengan XXX...," dan memberikan ssABteAstACABnnACsssebagai input contoh . Jawaban ini berfungsi untuk contoh itu , tetapi tidak menjawab pertanyaan secara umum. Sebagai contoh, ssABteCstACABnnACssseharusnya juga menghasilkan output aaXXXABnnACss, tetapi perintah Anda melewati baris ini melalui tidak berubah.
G-Man Mengatakan 'Reinstate Monica'
0

Solusinya cukup sederhana. .*serakah, tetapi tidak sepenuhnya serakah. Pertimbangkan untuk mencocokkan ssABteAstACABnnACssdengan regexp AB.*AC. Yang ACmengikuti .*harus benar-benar memiliki kecocokan. Masalahnya adalah karena .*serakah, yang berikutnya ACakan cocok dengan yang terakhir AC daripada yang pertama. .*memakan yang pertama ACsedangkan literal ACdi regexp cocok dengan yang terakhir di ssABteAstACABnn AC ss. Untuk mencegah hal ini terjadi, gantilah yang pertama ACdengan yang konyol untuk membedakannya dari yang kedua dan yang lainnya.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

Serakah .*akan berhenti di kaki -foobar-di ssABteAst-foobar-ABnnACsskarena tidak ada yang lain -foobar-dari ini -foobar-, dan regexp -foobar- HARUS memiliki kecocokan. Masalah sebelumnya adalah bahwa regexp ACmemiliki dua pertandingan, tetapi karena .*serakah, pertandingan terakhir untuk ACdipilih. Namun, dengan -foobar-, hanya satu pertandingan yang memungkinkan, dan pertandingan ini membuktikan bahwa .*itu tidak sepenuhnya serakah. Halte bus untuk .*terjadi di mana hanya satu pertandingan tersisa untuk sisa regexp berikut .*.

Perhatikan bahwa solusi ini akan gagal jika ACmuncul sebelum yang pertama ABkarena yang salah ACakan diganti -foobar-. Misalnya, setelah sedsubstitusi pertama , ACssABteAstACABnnACssmenjadi -foobar-ssABteAstACABnnACss; oleh karena itu, kecocokan tidak dapat ditemukan melawan AB.*-foobar-. Namun, jika urutannya selalu ... AB ... AC ... AB ... AC ..., maka solusi ini akan berhasil.

JD Graham
sumber
0

Salah satu alternatif adalah mengubah string sehingga Anda menginginkan kecocokan serakah

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Gunakan revuntuk membalikkan string, membalikkan kriteria pertandingan Anda, gunakan seddengan cara biasa dan kemudian balikkan hasilnya ....

ssAB-+-+-+-+ACABnnACss
bu5hman
sumber