Sed - Zamień pierwsze k wystąpień słowa w pliku

24

Chcę zastąpić tylko pierwsze kwystąpienia słowa.

W jaki sposób mogę to zrobić?

Na przykład. Powiedz plik foo.txtzawiera 100 wystąpień słowa „linux”.

Muszę wymienić tylko pierwsze 50 wystąpień.

narendra-choudhary
źródło
1
Możesz odnieść się do tego: unix.stackexchange.com/questions/21178/…
cuonglm
Czy potrzebujesz konkretnie lub czy inne narzędzia są dopuszczalne? Czy potrzebujesz pracować w wierszu poleceń, czy akceptujesz edytor tekstowy?
evilsoup
Wszystko, co działa w wierszu poleceń, jest dopuszczalne.
narendra-choudhary

Odpowiedzi:

31

Pierwsza sekcja poniżej opisuje użycie seddo zmiany pierwszych k-wystąpień na linii. Druga sekcja rozszerza to podejście, aby zmienić tylko pierwsze k-wystąpienia w pliku, niezależnie od tego, w której linii się pojawiają.

Rozwiązanie zorientowane liniowo

W przypadku standardowego sed istnieje polecenie zastąpienia k-tego wystąpienia słowa w wierszu. Jeśli kwynosi 3, na przykład:

sed 's/old/new/3'

Lub można zastąpić wszystkie wystąpienia:

sed 's/old/new/g'

Żadne z nich nie jest tym, czego chcesz.

GNU sedoferuje rozszerzenie, które zmieni k-te wystąpienie, a potem. Jeśli k wynosi 3, na przykład:

sed 's/old/new/g3'

Można je łączyć, aby robić, co chcesz. Aby zmienić pierwsze 3 wystąpienia:

$ 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

gdzie \njest to przydatne, ponieważ możemy być pewni, że nigdy nie występuje na linii.

Wyjaśnienie:

Używamy trzech sedpoleceń podstawienia:

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

    To rozszerzenie GNU zastąpić czwarty i wszystkie kolejne wystąpienia oldz \n.

    Rozszerzona funkcja wyrażenia regularnego \<służy do dopasowania początku słowa i \>dopasowania do końca słowa. Zapewnia to, że dopasowywane są tylko pełne słowa. Rozszerzone wyrażenie regularne wymaga -Eopcji sed.

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

    Pozostały tylko trzy pierwsze wystąpienia, oldco zastępuje je wszystkie new.

  • s/\n/old/g

    Czwarte i wszystkie pozostałe wystąpienia oldzostały zastąpione \nw pierwszym kroku. To przywraca ich pierwotny stan.

Rozwiązanie inne niż GNU

Jeśli GNU sed nie jest dostępny i chcesz zmienić pierwsze 3 wystąpienia oldna new, użyj trzech spoleceń:

$ 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

Działa to dobrze, gdy kjest mała, ale skaluje się słabo do dużej k.

Ponieważ niektóre sedy inne niż GNU nie obsługują łączenia poleceń ze średnikami, każde polecenie tutaj jest wprowadzane z własną -eopcją. Może być również konieczne sprawdzenie, czy sedobsługujesz symbole granic słów, \<oraz \>.

Rozwiązanie zorientowane na pliki

Możemy nakazać sedowi odczytanie całego pliku, a następnie wykonanie podstawień. Na przykład, aby zastąpić pierwsze trzy wystąpienia oldużycia sed w stylu BSD:

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

Polecenia sed H;1h;$!d;xodczytują cały plik.

Ponieważ powyższe nie używa żadnego rozszerzenia GNU, powinno działać na sedku BSD (OSX). Należy pamiętać, że takie podejście wymaga sedobsługi długich linii. GNU sedpowinno być w porządku. Osoby używające wersji innej niż GNU sedpowinny przetestować swoją zdolność do obsługi długich linii.

W przypadku GNU sed możemy dalej wykorzystać glewę opisaną powyżej, ale z \nzastąpioną przez \x00, aby zastąpić pierwsze trzy wystąpienia:

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

To podejście dobrze się skaluje i kstaje się duże. Zakłada się jednak, że \x00nie ma go w oryginalnym ciągu. Ponieważ niemożliwe jest umieszczenie znaku \x00w ciągu bash, jest to zazwyczaj bezpieczne założenie.

John1024
źródło
5
Działa to tylko w przypadku linii i zmieni pierwsze 4 wystąpienia w każdej linii
1
@mikeserv Doskonały pomysł! Odpowiedź zaktualizowana.
John1024,
(1) Wspierasz GNU i nie-GNU sed i sugerujesz tr '\n' '|' < input_file | sed …. Ale, oczywiście, to przekształca cały sygnał wejściowy w jedną linię, a niektóre sedy inne niż GNU nie mogą obsługiwać dowolnie długich linii. (2) Mówisz: „… powyżej cytowany ciąg '|'powinien zostać zastąpiony dowolnym znakiem lub ciągiem znaków,…” Ale nie możesz użyć, traby zastąpić znak ciągiem (o długości> 1). (3) W swoim ostatnim przykładzie mówisz -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Wydaje się, że to literówka -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man mówi „Przywróć Monikę”
@ G-Man Dzięki wielkie! Zaktualizowałem odpowiedź.
John1024,
to takie brzydkie
Louis Maddox
8

Korzystanie z Awk

Poleceń awk można użyć do zastąpienia pierwszych N wystąpień słowa zamiennikiem.
Polecenia zostaną zastąpione tylko wtedy, gdy słowo jest w pełni zgodne.

W poniższych przykładach, jestem zastępując pierwsze 27wystąpienia oldznew

Korzystanie z sub

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

To polecenie zapętla każde pole, aż się dopasuje old, sprawdza, czy licznik jest poniżej 27, zwiększa i zastępuje pierwsze dopasowanie w linii. Następnie przechodzi do następnego pola / linii i powtarza się.

Wymiana pola ręcznie

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

Podobnie jak wcześniej polecenie, ale ponieważ ma już znacznik, na którym polu ma zamiar ($i), po prostu zmienia wartość pola z oldna new.

Przeprowadzanie kontroli wcześniej

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

Sprawdzanie, czy linia zawiera stary i czy licznik jest poniżej 27, SHOULDzapewnia niewielkie zwiększenie prędkości, ponieważ nie będzie przetwarzać linii, gdy są one fałszywe.

WYNIKI

Na przykład

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

do

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
źródło
Pierwszy (używając sub) robi coś złego, jeśli ciąg „stary” poprzedza * słowo stary; np. „Daj trochę złota staremu człowiekowi.” → „Daj trochę świeżego starego człowiekowi”.
G-Man mówi „Przywróć Monikę”
@ G-Man Tak, zapomniałem $itrochę, został zredagowany, dzięki :)
7

Powiedz, że chcesz zastąpić tylko trzy pierwsze wystąpienia ciągu ...

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

uwaga: powyższe prawdopodobnie nie będzie działać z osadzonymi komentarzami
... lub w moim przykładzie przypadku „1” ...

WYDAJNOŚĆ:

22
211
211
311

Tam używam dwóch znaczących technik. Przede wszystkim każde wystąpienie 1na linii jest zastępowane przez \n1. W ten sposób, wykonując następnie zamiany rekurencyjne, mogę być pewien, że nie zastąpię wystąpienia dwukrotnie, jeśli mój ciąg zastępujący zawiera mój ciąg zastępujący. Na przykład, jeśli mogę wymienić hez heynim będzie nadal działać.

Robię to tak:

s/1/\
&/g

Po drugie, liczę zamienniki, dodając znak do hstarego miejsca dla każdego wystąpienia. Gdy osiągnę trzy, nie będzie już więcej. Jeśli zastosujesz to do swoich danych i zmienisz \{3\}całkowitą liczbę żądanych zamienników oraz /\n1/adresy na cokolwiek, co chcesz zastąpić, powinieneś wymienić tylko tyle, ile chcesz.

Zrobiłem wszystkie te -erzeczy dla czytelności. POSIXly Można to napisać w ten sposób:

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

I w / GNU sed:

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

Pamiętaj też, że sedjest on zorientowany liniowo - nie czyta całego pliku, a następnie próbuje zapętlić go z powrotem, jak to często bywa w innych edytorach. sedjest prosty i wydajny. To powiedziawszy, często wygodnie jest zrobić coś takiego:

Oto mała funkcja powłoki, która łączy ją w prosto wykonane polecenie:

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

Dzięki temu mogę zrobić:

seq 11 100 311 | firstn 7 1 5

...i dostać...

55
555
255
311

...lub...

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

... żeby dostać ...

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

... lub, aby dopasować swój przykład (o mniejszym rzędzie wielkości) :

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
źródło
4

Krótka alternatywa w Perlu:

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

Zmień wartość „$ n $ na swoje upodobania.

Jak to działa:

  • Dla każdej linii, to wciąż stara się zastąpić newprzez old( s/old/new/) i gdy to możliwe, zwiększa ona zmienną $i( ++$i).
  • Nadal działa na linii ( 1 while ...), o ile $nw sumie dokonał mniej niż podstawień i może dokonać co najmniej jednego podstawienia w tym wierszu.
Joseph R.
źródło
4

Użyj pętli powłoki i ex!

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

Tak, to trochę głupie.

;)

Uwaga: Może się to nie powieść, jeśli oldw pliku jest mniej niż 50 wystąpień . (Nie przetestowałem tego.) Jeśli tak, plik pozostanie niezmodyfikowany.


Jeszcze lepiej, użyj Vima.

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

Wyjaśnienie:

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
Dzika karta
źródło
: s // nowy <CR> powinien również działać, ponieważ pusty wyrażenie regularne ponownie wykorzystuje ostatnio używane wyszukiwanie
jak
3

Prostym, ale niezbyt szybkim rozwiązaniem jest zapętlenie poleceń opisanych w /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -plik

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

Ta konkretna komenda sed prawdopodobnie działa tylko dla GNU sed i jeśli newword nie jest częścią oldword . W przypadku wersji innych niż GNU zobacz tutaj, jak zastąpić tylko pierwszy wzorzec w pliku.

Jofel
źródło
+1 za identyfikację, że zamiana „starego” na „pogrubienie” może powodować problemy.
G-Man mówi „Przywróć Monikę”
2

Za pomocą GNU awkmożesz ustawić separator rekordów RSna słowo, które ma być zastąpione ograniczeniem przez granice słów. Jest to przypadek ustawienia separatora rekordów na wyjściu na słowo zastępcze dla pierwszych krekordów, przy zachowaniu oryginalnego separatora rekordów dla reszty

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

LUB

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