Sed - Ganti k contoh pertama dari kata dalam file

24

Saya ingin mengganti hanya kcontoh kata pertama.

Bagaimana saya bisa melakukan ini?

Misalnya. Katakanlah file foo.txtberisi 100 kejadian kata 'linux'.

Saya perlu mengganti 50 kejadian pertama saja.

narendra-choudhary
sumber
1
Anda dapat merujuk ini: unix.stackexchange.com/questions/21178/…
cuonglm
Apakah Anda memerlukan sed secara khusus, atau apakah alat lain dapat diterima? Apakah Anda perlu bekerja pada baris perintah, atau apakah editor teks dapat diterima?
evilsoup
Apa pun yang bekerja pada baris perintah dapat diterima.
narendra-choudhary

Jawaban:

31

Bagian pertama di bawah ini menjelaskan penggunaan seduntuk mengubah kejadian-k pertama pada suatu garis. Bagian kedua memperluas pendekatan ini untuk mengubah hanya kejadian-k pertama dalam file, terlepas dari apa baris mereka muncul.

Solusi berorientasi garis

Dengan sed standar, ada perintah untuk mengganti kemunculan kata ke-k pada sebuah baris. Jika k3, misalnya:

sed 's/old/new/3'

Atau, seseorang dapat mengganti semua kejadian dengan:

sed 's/old/new/g'

Tidak satu pun dari ini yang Anda inginkan.

GNU sedmenawarkan ekstensi yang akan mengubah kejadian k-th dan semuanya setelah itu. Jika k adalah 3, misalnya:

sed 's/old/new/g3'

Ini dapat digabungkan untuk melakukan apa yang Anda inginkan. Untuk mengubah 3 kejadian pertama:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

di mana \nberguna di sini karena kita dapat yakin bahwa itu tidak pernah terjadi pada suatu garis.

Penjelasan:

Kami menggunakan tiga sedperintah substitusi:

  • s/\<old\>/\n/g4

    Ini ekstensi GNU untuk menggantikan yang keempat dan semua kejadian berikutnya olddengan \n.

    Fitur regex diperpanjang \<digunakan untuk mencocokkan awal kata dan \>untuk mencocokkan akhir kata. Ini memastikan bahwa hanya kata-kata lengkap yang cocok. Regex yang diperluas membutuhkan -Eopsi untuk sed.

  • s/\<old\>/new/g

    Hanya tiga kejadian pertama yang oldtersisa dan ini menggantikan semuanya new.

  • s/\n/old/g

    Kejadian keempat dan semua yang tersisa olddigantikan dengan \npada langkah pertama. Ini mengembalikan mereka ke keadaan semula.

Solusi non-GNU

Jika GNU sed tidak tersedia dan Anda ingin mengubah 3 kejadian pertama oldmenjadi new, maka gunakan tiga sperintah:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

Ini bekerja dengan baik ketika ksejumlah kecil tetapi skala buruk ke besar k.

Karena beberapa sed non-GNU tidak mendukung menggabungkan perintah dengan titik koma, setiap perintah di sini diperkenalkan dengan -eopsi sendiri . Mungkin juga perlu untuk memverifikasi bahwa Anda sedmendukung simbol batas kata, \<dan \>.

Solusi berorientasi file

Kita dapat meminta sed untuk membaca seluruh file dan kemudian melakukan penggantian. Misalnya, untuk mengganti tiga kejadian pertama oldmenggunakan sed gaya BSD:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

Perintah sed H;1h;$!d;xmembaca seluruh file di.

Karena di atas tidak menggunakan ekstensi GNU, itu harus bekerja pada BSD (OSX) sed. Perhatikan, pikirkan, bahwa pendekatan ini membutuhkan sedyang dapat menangani garis panjang. GNU sedseharusnya baik-baik saja. Mereka yang menggunakan versi non-GNU sedharus menguji kemampuannya untuk menangani antrean panjang.

Dengan sed GNU, kita dapat lebih lanjut menggunakan gtrik yang dijelaskan di atas, tetapi dengan \ndiganti dengan \x00, untuk mengganti tiga kejadian pertama:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

Pendekatan ini berskala juga kmenjadi besar. Ini mengasumsikan, bahwa \x00itu tidak ada dalam string asli Anda. Karena tidak mungkin untuk menempatkan karakter \x00dalam string bash, ini biasanya merupakan asumsi yang aman.

John1024
sumber
5
Ini hanya berfungsi untuk baris dan akan mengubah 4 kejadian pertama di setiap baris
1
@ mikeserv Ide bagus! Jawaban diperbarui.
John1024
(1) Anda menyebutkan GNU dan bukan GNU, dan menyarankan tr '\n' '|' < input_file | sed …. Tetapi, tentu saja, itu mengubah seluruh input menjadi satu baris, dan beberapa sed non-GNU tidak dapat menangani garis panjang yang sewenang-wenang. (2) Anda berkata, "... di atas, string yang dikutip '|'harus diganti oleh karakter apa pun, atau string karakter, ..." Tetapi Anda tidak dapat menggunakan truntuk mengganti karakter dengan string (panjang> 1). (3) Dalam contoh terakhir Anda, Anda katakan -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Ini sepertinya salah ketik untuk -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man Mengatakan 'Reinstate Monica'
@ G-Man Terima kasih banyak! Saya sudah memperbarui jawabannya.
John1024
ini sangat jelek
Louis Maddox
8

Menggunakan Awk

Perintah awk dapat digunakan untuk mengganti N kejadian pertama kata dengan penggantian.
Perintah hanya akan menggantikan jika kata tersebut benar-benar cocok.

Dalam contoh di bawah ini, saya mengganti 27kejadian pertama olddengannew

Menggunakan sub

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

Perintah ini melewati setiap bidang hingga cocok old, memeriksa penghitung di bawah 27, peningkatan dan mengganti kecocokan pertama pada baris. Kemudian pindah ke bidang / baris berikutnya dan ulangi.

Mengganti bidang secara manual

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Mirip dengan perintah sebelumnya tetapi karena sudah memiliki penanda di bidang mana itu hingga ($i), itu hanya mengubah nilai bidang dari oldke new.

Melakukan pemeriksaan sebelumnya

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

Memeriksa bahwa baris berisi yang lama dan penghitung di bawah 27 SHOULDmemberikan dorongan kecepatan kecil karena tidak akan memproses garis ketika ini salah.

HASIL

Misalnya

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

untuk

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old
Jeff Schaller
sumber
Yang pertama (menggunakan sub) melakukan hal yang salah jika string "lama" mendahului * kata lama; mis., “Berikan emas kepada lelaki tua itu.” → “Berikan beberapa hadiah kepada lelaki tua itu.”
G-Man Berkata 'Reinstate Monica'
@ G-Man Ya saya lupa $ibitnya, sudah diedit, terima kasih :)
7

Katakanlah Anda ingin mengganti hanya tiga contoh pertama dari string ...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

catatan: di atas kemungkinan tidak akan berfungsi dengan komentar yang disematkan
... atau dalam contoh kasus saya, dari '1' ...

KELUARAN:

22
211
211
311

Di sana saya menggunakan dua teknik penting. Di tempat pertama setiap kemunculan 1pada satu baris diganti dengan \n1. Dengan cara ini, ketika saya melakukan penggantian rekursif berikutnya, saya bisa pastikan untuk tidak mengganti kejadian dua kali jika string pengganti saya berisi string pengganti saya. Misalnya, jika saya ganti hedengan heyitu masih akan berfungsi.

Saya melakukan ini seperti:

s/1/\
&/g

Kedua, saya menghitung penggantian dengan menambahkan karakter ke hruang lama untuk setiap kejadian. Begitu saya mencapai tiga tidak ada lagi terjadi. Jika Anda menerapkan ini pada data Anda dan mengubah \{3\}ke penggantian total yang Anda inginkan dan /\n1/alamat untuk apa pun yang Anda ingin ganti, Anda harus mengganti hanya sebanyak yang Anda inginkan.

Saya hanya melakukan semua -ehal untuk dibaca. POSIXly Dapat ditulis seperti ini:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

Dan dengan GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

Ingat juga bahwa sedini berorientasi garis - tidak membaca di seluruh file dan kemudian mencoba untuk mengulanginya seperti yang sering terjadi pada editor lain. sedsederhana dan efisien. Yang mengatakan, sering kali nyaman untuk melakukan sesuatu seperti berikut:

Berikut adalah fungsi shell kecil yang membundelnya menjadi perintah yang dieksekusi sederhana:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

Maka dengan itu saya bisa melakukan:

seq 11 100 311 | firstn 7 1 5

... dan dapatkan ...

55
555
255
311

...atau...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

...mendapatkan...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... atau, untuk mencocokkan contoh Anda (dengan urutan yang lebih kecil) :

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux
mikeserv
sumber
4

Alternatif singkat di Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

Ubah nilai `$ n $ sesuai keinginan Anda.

Bagaimana itu bekerja:

  • Untuk setiap baris, itu terus mencoba untuk mengganti newuntuk old( s/old/new/) dan kapan pun bisa, itu akan menambahkan variabel $i( ++$i).
  • Itu terus bekerja pada baris ( 1 while ...) selama itu telah membuat kurang dari $ntotal substitusi dan dapat membuat setidaknya satu substitusi pada baris itu.
Joseph R.
sumber
4

Gunakan loop shell dan ex!

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

Ya, ini agak konyol.

;)

Catatan: Ini mungkin gagal jika ada kurang dari 50 contoh olddalam file. (Saya belum mengujinya.) Jika demikian, itu akan membuat file tidak dimodifikasi.


Lebih baik lagi, gunakan Vim.

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

Penjelasan:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit
Wildcard
sumber
: s // new <CR> juga berfungsi, karena regex kosong menggunakan kembali pencarian yang terakhir digunakan
eike
3

Solusi sederhana, tetapi tidak terlalu cepat adalah untuk mengulang perintah yang dijelaskan dalam /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -mengajukan

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

Perintah sed khusus ini mungkin hanya berfungsi untuk GNU sed dan jika newword bukan bagian dari oldword . Untuk non-GNU lihat di sini cara mengganti hanya pola pertama dalam file.

Jofel
sumber
+1 untuk mengidentifikasi bahwa mengganti "lama" dengan "tebal" dapat menyebabkan masalah.
G-Man Mengatakan 'Reinstate Monica'
2

Dengan GNU awkAnda dapat mengatur pemisah rekaman RSke kata yang akan diganti dibatasi oleh batas kata. Maka itu adalah kasus pengaturan pemisah rekaman pada output ke kata pengganti untuk kcatatan pertama sambil mempertahankan pemisah rekaman asli untuk sisanya.

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

ATAU

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
iruvar
sumber