Jak usunąć wiersz, jeśli zawiera on znak dokładnie raz

10

Chcę usunąć wiersz z pliku, który zawiera określony znak tylko raz, jeśli jest obecny więcej niż jeden raz lub nie jest obecny, zachowaj ten wiersz w pliku.

Na przykład:

DTHGTY
FGTHDC
HYTRHD
HTCCYD
JUTDYC

Tu, postać którą chcesz usunąć to Ctak, komenda powinna usunąć linie FGTHDCi JUTDYCponieważ mają Cdokładnie jeden raz.

W jaki sposób można to zrobić przy użyciu albo sedczy awk?

Namz
źródło

Odpowiedzi:

20

W awkmożesz ustawić separator pól na cokolwiek. Jeśli ustawisz tę wartość C, będziesz mieć tyle pól +1, ile wystąpień C.

Więc jeśli powiesz, awk -F'C' '{print NF}' <<< "C1C2C3"że dostajesz 4: CCCskłada się z 3 Cs, a zatem 4 pól.

Chcesz usunąć linie, w których Cwystępuje dokładnie raz. Biorąc to pod uwagę, w twoim przypadku będziesz chciał usunąć te linie, w których są dokładnie dwa Cpola. Więc po prostu je pomiń:

$ awk -F'C' 'NF!=2' file
DTHGTY
HYTRHD
HTCCYD
fedorqui
źródło
4
Sprytne użycie awkseparatora pól!
Valentin B.
interresting, jak w przypadku domyślnym (FS = "") ignoruje spacje wiodące ($ 1 = pierwsza spacja w linii), a także powtórzenia (możesz mieć 5 spacji do oddzielenia pola 1 i pola 2) ... spacja jest prawdopodobnie traktowany specjalnie? (aby to zobaczyć, można to zrobić awk 'BEGIN { print "FS={" FS"}","OFS={" OFS "}";} {printf "%d fields : ",NF; for (i=1;i<=NF;i++) {printf "{" $i "} ";}; print "" }'i nakarmić go niektórymi wierszami, niektóre mające wiele szpiegów, a inne zaczynają się od spacji)
Olivier Dulac
2
@OlivierDulac, tak, spacja jest obsługiwana specjalnie w sposób określony przez POSIX .
Wildcard
8

podejście sed :

sed -i '/^[^C]*C[^C]*$/d' input

-i opcja pozwala na modyfikację plików w miejscu

/^[^C]*C[^C]*$/- dopasowuje wiersze zawierające Ctylko jeden raz

d - usuń dopasowane linie

Roman Perekhrest
źródło
8

Można to zrobić za pomocą sed:

Kod:

sed '/C.*C/p;/C/d' file1

Wyniki:

DTHGTY
HYTRHD
HTCCYD

W jaki sposób?

  1. Dopasuj i wydrukuj dowolną linię za pomocą co najmniej dwóch kopii Cvia/C.*C/p
  2. Usuń dowolną linię za pomocą Cvia /C/d, dotyczy to również linii wydrukowanych już w kroku 1
  3. Domyślnie drukuje pozostałe linie
Stephen Rauch
źródło
2
Sprytne podejście alternatywne; Lubię to.
Wildcard
6

To usuwa linie z dokładnie jednym wystąpieniem C.

grep -v '^[^C]*C[^C]*$' file

Wyrażenie regularne [^C]pasuje do jednego znaku, który nie jest C (lub znakiem nowej linii), a operator powtarzania (aka gwiazda Kleene) *określa zero lub więcej powtórzeń poprzedniego wyrażenia.

Domyślnym wyjściem grep(i większości innych narzędzi tekstowych) jest wyjście standardowe; przekieruj do nowego pliku i może przenieś go na oryginalny plik, jeśli tego chcesz. Tego samego wyrażenia regularnego można używać sed -ido edycji w miejscu:

sed -i '/^[^C]*C[^C]*$/d' file

(Na niektórych platformach, zwłaszcza * BSD, w tym macOS, -iopcja wymaga argumentu, takiego jak -i ''.)

potrójny
źródło
1
sed -i '/^[^C]*C[^C]*$/d' file- brzmi tak, jakby był opublikowany wcześniej, jak myślisz, plagiat?
RomanPerekhrest
1
Rzeczywiście istnieje pewne powielanie. Zacząłem od grepodpowiedzi, która oczywiście łatwo rozszerza się na sed -iwariant. Nie widziałem twojej odpowiedzi, ponieważ szukałem poprzednich grepodpowiedzi.
tripleee
1
Bezpieczniej jest po prostu po prostu unikać -ize sedi zamiast przekierowywać do nowego pliku i zastąpić oryginał że jeśli sednarzędzie wyszedł bez błędu.
Kusalananda
2
Lubgrep -vx '[^C]*C[^C]*'
Stéphane Chazelas
@Kusalananda Ale równie dobrze możesz użyć, grepponieważ jest jaśniejszy i bardziej niezawodny (w szczególności sedma mniej informacyjny kod wyjścia).
tripleee
4

Narzędzie POSIX do skryptowych edycji pliku (zamiast drukowania zmodyfikowanej zawartości na standardowe wyjście) to ex.

printf '%s\n' 'g/^[^C]*C[^C]*$/d' x | ex file.txt

Oczywiście możesz go użyć,sed -i jeśli Twoja wersja Sed go obsługuje, ale pamiętaj, że nie jest przenośny, jeśli piszesz skrypt przeznaczony do uruchamiania na różnych typach systemów.


David Foerster zapytał w komentarzach:

Czy jest jakiś powód, dla którego używasz printf, a nie echoczy coś takiego ex -c COMMAND?

Odpowiedź: Tak

Dla printfkontra echojest to kwestia przenośności; zobacz Dlaczego printf jest lepszy niż echo? Łatwiej jest także przeplatać znaki nowej linii między poleceniami printf.

Dla printf ... | exkontra ex -c ...jest to kwestia obsługi błędów. Dla tego konkretnego polecenia nie miałoby to znaczenia, ale ogólnie ma to znaczenie; na przykład spróbuj umieścić

ex -c '%s/this pattern is not in the file/replacement text/g | x' filename

w skrypcie. Porównaj z następującymi:

printf '%s\n' '%s/no matching lines/replacement/g' x | ex file

Pierwszy zawiesza się i oczekuje na wejście; drugi zakończy działanie po otrzymaniu EOF przez expolecenie, więc skrypt będzie kontynuowany. Istnieją alternatywne obejścia, takie jak s///e, ale nie są one określone przez POSIX. Wolę używać przenośnego formularza, który pokazano powyżej.

W przypadku gpolecenia na końcu musi znajdować się nowa linia i wolę używać printfzawijania poleceń zamiast osadzania nowej linii w pojedynczych cudzysłowach.

Dzika karta
źródło
1
Czy jest jakiś powód, dla którego używasz printf, a nie echoczy coś takiego ex -c COMMAND?
David Foerster
@DavidFoerster, tak. Zacząłem odpowiadać w komentarzach, ale wydłużyło się, więc dodałem to do odpowiedzi.
Wildcard
Dzięki i +1! Wiedziałem o printfkontra echo(chociaż zazwyczaj wolę, echogdy argument jest na stałe zakodowany), ale do tej pory nie używałem go zbyt często ex.
David Foerster
2

Oto kilka opcji przy użyciu Perla.

Ponieważ dopasowujesz tylko jeden znak, możesz użyć tr/C//(tłumaczenie, bez zamienników), aby zwrócić liczbę dopasowań C:

perl -lne 'print if tr/C// != 1' file

Mówiąc bardziej ogólnie, jeśli chcesz dopasować ciąg znaków lub wyrażenie regularne, możesz użyć tego:

perl -lne 'print if (@m = /C/g) != 1' file

To przypisuje dopasowania wyrażenia regularnego /C/gdo listy @mi drukuje linie, gdy długość tej listy nie jest 1.

-iPrzełącznik mogą być dodawane do edycji „w miejscu”.

Tom Fenech
źródło
2
sed -e '
  s/C/&/2;t   # when 2nd C matches skip processing and print
  /C/d        # either one C or no C, so delete on C
'

sed -e '
   /C/!b     # no C, skip processing and print
   /C.*C/!d  # not(at least 2 C) => 1 C => delete
'

perl -lne 's/C/C/g == 1 or print'

źródło
Zauważ, że zakłada GNU sed, t #...zwykle rozgałęzia się do etykiety wywoływanej #...w większości innych sedimplementacji.
Stéphane Chazelas
Nawet !bGNU sed, ponieważ gałąź nie lubi niczego oprócz etykiety lub nowego wiersza po nim.
Tak b, t, :, }(i r file, w file...) mogą nie mieć polecenia za nimi na tej samej linii. Możesz także użyć osobnych -eopcji.
Stéphane Chazelas
Twoja opcja perla nie generuje poprawnego wyniku. Chyba zapomniałeś dodać gmodyfikator.
Tom Fenech
@TomFenech Masz rację. Naprawiam to. Dzięki.
1

Dla każdego, kto chce awkkonkretnie, zaoferowałbym

awk '/C[^C]*C/{next}//{print}'

pomiń linię, jeśli pasuje do wzoru, wydrukuj w przeciwnym razie. Tak naprawdę nie potrzebujesz {print}, możesz użyć //domyślnego wydruku, ale myślę, że jest to wyraźniejsze.

Moją pierwszą myślą było użycie egrep -vtego samego wzoru, ale tak naprawdę to nie odpowiada na postawione pytanie.

nigel222
źródło
1
Po co więc coś pasować {next}? Po prostu powiedz, awk '/pattern/ {next} 1'a wszystkie linie nie pasujące do wzoru zostaną wydrukowane. Lub, lepiej, awk '!/pattern/'bezpośrednio je wydrukować.
fedorqui
@fedorqui dobra uwaga na temat !/pattern/(co jakoś wymknęło mi się z głowy), ale zdecydowanie wolałbym, aby było to zrozumiałe //{print}niż tajemnicze 1. Zakładaj, że Twój kod zachowuje jak najmniej kompetencji i biegłości od następnej osoby, co nie oznacza, że ​​jest on znacznie mniej wydajny lub efektywny.
nigel222