Sed - Замініть перші k екземпляри слова у файлі


24

Я хочу замінити лише перші kекземпляри слова.

Як я можу це зробити?

Напр. Скажімо, файл foo.txtмістить 100 екземплярів слова "linux".

Мені потрібно замінити лише перші 50 випадків.


1
Ви можете посилатися на це: unix.stackexchange.com/questions/21178/…
cuonglm

Вам потрібен sed конкретно, або інші інструменти прийнятні? Чи потрібно працювати в командному рядку чи прийнятний текстовий редактор?
evilsoup

Все, що працює в командному рядку, є прийнятним.
нарендра-чодхарі

Відповіді:


31

У першому розділі нижче описано використання sedдля зміни перших k-входжень у рядку. Другий розділ розширює цей підхід, щоб змінити лише перші k-входження у файлі, незалежно від того, на якому рядку вони відображаються.

Лінійно-орієнтоване рішення

За допомогою стандартного sed є команда замінити k-ту появу слова на рядку. Якщо kце 3, наприклад:

sed 's/old/new/3'

Або можна замінити всі події на:

sed 's/old/new/g'

Жодне з них не є тим, чого ви хочете.

GNU sedпропонує розширення, яке змінить k-ту появу і все після цього. Якщо k дорівнює 3, наприклад:

sed 's/old/new/g3'

Їх можна комбінувати, щоб робити те, що ти хочеш. Щоб змінити перші 3 події:

$ 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

де \nце корисно, тому що ми можемо бути впевнені, що це ніколи не трапляється на лінії.

Пояснення:

Ми використовуємо три sedкоманди підстановки:

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

    Це розширення ГНУ замінити четвертий і всі наступні входження oldз \n.

    Функція розширеного регулярного виразів \<використовується для того, щоб відповідати початку слова та \>відповідати кінці слова. Це запевняє, що збігаються лише цілі слова. Розширений регулярний вираз вимагає -Eопції для sed.

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

    oldЗалишаються лише перші три випадки, і це замінює їх усіх new.

  • s/\n/old/g

    Четвертий і всі залишки, що залишилися, oldбули замінені \nна першому кроці. Це повертає їх до початкового стану.

Рішення, що не стосується GNU

Якщо GNU sed недоступний і ви хочете змінити перші 3 входи oldна new, використовуйте три sкоманди:

$ 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

Це добре працює, коли kце невелика кількість, але масштабується погано до великої k.

Оскільки деякі набори, що не належать до GNU, не підтримують комбінування команд із крапкою з комою, тут кожна команда вводиться із власною -eопцією. Також може знадобитися переконатися, що sedпідтримує символи межі слова \<та \>.

Рішення, орієнтоване на файли

Ми можемо сказати sed прочитати весь файл, а потім виконати заміни. Наприклад, для заміни перших трьох випадків oldвикористання sed-стилю BSD:

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

Команди sed H;1h;$!d;xчитають весь файл у.

Оскільки вище не використовується жодне розширення GNU, воно повинно працювати на BSD (OSX) sed. Зауважте, подумав, що цей підхід вимагає того, sedщо може обробляти довгі лінії. GNU sedмає бути добре. Тим, хто використовує не GNU версію, sedслід перевірити її здатність обробляти довгі лінії.

За допомогою GNU sed ми можемо додатково використовувати gтрюк, описаний вище, але \nзамінений на \x00, щоб замінити перші три випадки:

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

Цей підхід масштабується і kстає великим. Це передбачає, що \x00це не у вашій початковій рядку. Оскільки неможливо поставити символу \x00в баш-рядок, зазвичай це безпечне припущення.


5
Це працює лише для ліній і змінить перші 4 випадки в кожному рядку

1
@mikeserv Відмінна ідея! Відповідь оновлено.
John1024

(1) Ви згадуєте GNU та non-GNU sed, і пропонуєте tr '\n' '|' < input_file | sed …. Але, звичайно, це перетворює весь вхід в один рядок, і деякі не-GNU набори не можуть обробляти довільно довгі рядки. (2) Ви говорите: "... вище, цитований рядок '|'слід замінити будь-яким символом або рядком символів, ..." Але ви не можете використовувати trдля заміни символу рядком (довжиною> 1). (3) Ви говорите в своєму останньому прикладі -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Це здається помилковим -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man каже: "Відновіть Моніку"

@ G-Man Спасибі велике! Я оновив відповідь.
John1024

це так потворно
Луї Маддокс

8

Використання Awk

Команди awk можна використовувати для заміни перших N зустрічей слова заміною.
Команди заміняться лише у тому випадку, якщо слово є повним збігом.

У наведених нижче прикладах я замінюю перші 27подіїold зnew

Використовуючи під

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

Ця команда перебирає кожне поле, поки воно не збігається old , перевіряє лічильник нижче 27, з кроком і замінює перший збіг у рядку. Потім переходить на наступне поле / рядок і повторюється.

Заміна поля вручну

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

Подібно до команди раніше, але оскільки вона вже має маркер, на якому полі вона знаходиться ($i), вона просто змінює значення поля з oldнаnew .

Проведення перевірки раніше

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

Перевірка того, що рядок містить старі, а лічильник нижче 27, SHOULDдає невеликий приріст швидкості, оскільки він не буде обробляти лінії, коли вони помилкові.

РЕЗУЛЬТАТИ

Напр

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

до

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

Перший (використовуючи підпункт) робить неправильну справу, якщо рядок "старий" передує * старому слову ; наприклад, «Дайте трохи золота до старого.» → «Дайте деякий gnew до старого.»
G-Man говорить «відновила Моніку»

@ G-Man Так, я забув $iтрохи, його відредагували, дякую :)

7

Скажіть, що ви хочете замінити лише перші три екземпляри рядка ...

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

зауважте: вищевказане, ймовірно, не працюватиме із вбудованими коментарями
... або в моєму прикладі "1" ...

ВИХІД:

22
211
211
311

Там я використовую дві помітні методи. В першу чергу кожне виникнення 1на лінії замінюється на \n1. Таким чином, виконуючи рекурсивні заміни наступним чином, я можу бути впевнений, що не замінювати виникнення двічі, якщо мій рядок заміщення містить мій рядок заміни. Наприклад, якщо я замінюю heнаhey нею все одно буде працювати.

Я роблю це так:

s/1/\
&/g

По-друге, я рахую заміни, додаючи символ у hстарий пробіл для кожного події. Як тільки я досягну три більше не трапляються. Якщо ви застосуєте це до своїх даних і зміните \{3\}на загальну кількість замінників, які ви хочете, і/\n1/ адреси на те, що ви хочете замінити, вам слід замінити лише стільки, скільки ви хочете.

Я робив лише всі -eречі для читабельності. POSIXly Можна було б написати так:

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

І w / GNU sed:

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

Пам'ятайте також про те, що sedорієнтоване на рядки - він не читається у всьому файлі, а потім намагається повернути його назад, як це часто буває в інших редакторах. sedє простим і ефективним. Однак, часто зручно робити щось подібне:

Ось невелика функція оболонки, яка поєднує її в просто виконану команду:

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

Тож з цим я можу зробити:

seq 11 100 311 | firstn 7 1 5

... і отримуй ...

55
555
255
311

... або ...

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

...отримати...

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

... або, щоб відповідати вашому прикладу (на менший порядок) :

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

4

Коротка альтернатива в Perl:

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

Змініть значення `$ n $ на свій смак.

Як це працює:

  • Для кожного рядка, він продовжує намагатися замінити newна old( s/old/new/) і щоразу , коли це можливо, це збільшує змінну $i( ++$i).
  • Він продовжує працювати на лінії ( 1 while ...) до тих пір, поки зробив менше, ніж $nзаміни загалом, і він може зробити принаймні одну заміну на цій лінії.

4

Використовуйте петлю оболонки і ex!

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

Так, це трохи тупо.

;)

Примітка. Це може бути невдалим, якщо oldу файлі менше 50 екземплярів . (Я цього не перевіряв.) Якщо так, файл залишив би незмінним.


А ще краще використовувати Vim.

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

Пояснення:

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

: s // new <CR> також повинен працювати, оскільки порожній регулярний вираз повторно використовує останній пошук
eike

3

Просте, але не дуже швидке рішення - переходити до команд, описаних у /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -файл

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

Ця конкретна команда sed, ймовірно, працює лише для GNU sed, і якщо newword не є частиною старого слова . Для не-GNU sed дивіться тут, як замінити лише перший шаблон у файлі.


+1 для виявлення того, що заміна "старого" на "жирний" може спричинити проблеми.
G-Man каже: "Відновіть Моніку"

2

За допомогою GNU awkви можете встановити роздільник записів RSна слово, яке слід замінити, обмеженим межами слова. Тоді йдеться про встановлення розділювача записів на виході до слова заміщення для перших kзаписів, зберігаючи початковий роздільник записів на залишок

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

АБО

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.