Come rimuovere la linea se contiene un carattere esattamente una volta


10

Voglio rimuovere una riga da un file che contiene un determinato carattere solo una volta, se è presente più di una volta o non è presente, mantieni la riga nel file.

Per esempio:

DTHGTY
FGTHDC
HYTRHD
HTCCYD
JUTDYC

Qui, il personaggio che voglio rimuovere è Ccosì, il comando dovrebbe rimuovere le linee FGTHDCe JUTDYCperché hanno Cesattamente una volta.

Come posso farlo usando o sedo awk?

Risposte:


20

In awkpuoi impostare il separatore di campo su qualsiasi cosa. Se lo imposti su C, avrai tanti campi +1 quante occorrenze di C.

Quindi se dici awk -F'C' '{print NF}' <<< "C1C2C3"di ottenere 4: CCCconsiste in 3 Csecondi, e quindi in 4 campi.

Si desidera rimuovere le righe in cui si Cverifica esattamente una volta. Tenendo conto di questo, nel tuo caso vorrai rimuovere quelle linee in cui ci sono esattamente due Ccampi. Quindi saltali e basta:

$ awk -F'C' 'NF!=2' file
DTHGTY
HYTRHD
HTCCYD

4
Uso astuto del awkseparatore di campo!
Valentin B.,

l'interessante, come nel caso predefinito (FS = "") ignora gli spazi iniziali ($ 1 = il primo non-spazio sulla linea) e anche le ripetizioni (puoi avere 5 spazi per separare il campo 1 e il campo 2) ... spazio è probabilmente trattato in modo speciale? (per vederlo, si può fare awk 'BEGIN { print "FS={" FS"}","OFS={" OFS "}";} {printf "%d fields : ",NF; for (i=1;i<=NF;i++) {printf "{" $i "} ";}; print "" }'e dargli da mangiare alcune righe, alcune con più spces, e altre iniziano con spazi)
Olivier Dulac

2
@OlivierDulac, sì, lo spazio viene gestito appositamente come specificato da POSIX .
Wildcard

8

approccio sed :

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

-i opzione consente la modifica del file sul posto

/^[^C]*C[^C]*$/- corrisponde a righe che contengono Csolo una volta

d - elimina le linee corrispondenti


8

Questo può essere fatto con sed:

Codice:

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

risultati:

DTHGTY
HYTRHD
HTCCYD

Come?

  1. Abbina e stampa qualsiasi riga con almeno due copie di Cvia/C.*C/p
  2. Elimina qualsiasi riga con una Cvia /C/d, questo include le linee già stampate nel passaggio 1
  3. Predefinito stampa il resto delle righe

2
Approccio alternativo intelligente; Mi piace.
Wildcard

6

Ciò rimuove le righe con esattamente un'occorrenza di C.

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

L'espressione regolare [^C]corrisponde a un carattere che non è C (o newline) e l'operatore di ripetizione (aka stella di Kleene) *specifica zero o più ripetizioni dell'espressione precedente.

L'output predefinito grep(e la maggior parte degli altri strumenti orientati al testo) è l'output standard; reindirizzare a un nuovo file e magari spostarlo sopra il file originale se è quello che vuoi. Lo stesso regex può essere utilizzato con sed -iper la modifica sul posto:

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

(Su alcune piattaforme, in particolare * BSD incluso macOS, l' -iopzione richiede un argomento, come -i ''.)


1
sed -i '/^[^C]*C[^C]*$/d' file- sembra che sia stato pubblicato prima, come pensi, plagio?
RomanPerekhrest,

1
Anzi, c'è qualche duplicazione. Ho iniziato con la greprisposta, ma ovviamente si estende facilmente alla sed -ivariante. Non ho visto la tua risposta perché stavo cercando greprisposte precedenti .
Tripleee

1
È più sicuro evitare semplicemente -icon sede invece reindirizzare a un nuovo file e sostituire l'originale con quello se l' sedutilità è uscita senza errori.
Kusalananda

2
Oppuregrep -vx '[^C]*C[^C]*'
Stéphane Chazelas,

@Kusalananda Ma allora potresti anche usarlo grepperché è più chiaro e più robusto (in particolare, sedha un codice di uscita meno informativo).
Tripleee

4

Lo strumento POSIX per le modifiche tramite script di un file (piuttosto che stampare il contenuto modificato su standard out) è ex.

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

Ovviamente puoi usarlosed -i se la tua versione di Sed lo supporta, tieni presente che non è portatile se stai scrivendo uno script che dovrebbe essere eseguito su diversi tipi di sistemi.


David Foerster ha chiesto nei commenti:

C'è un motivo per cui stai usando printfe non echoo qualcosa del genere ex -c COMMAND?

Risposta: Sì

Per printfvs. echoè una questione di portabilità; vedi Perché printf è meglio dell'eco? Ed è anche più facile intercettare nuove righe tra i comandi usando printf.

Per printf ... | exvs. ex -c ..., è una questione di gestione degli errori. Per questo comando specifico non avrebbe importanza, ma in generale lo fa; per esempio, prova a mettere

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

in una sceneggiatura. In contrasto con quanto segue:

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

Il primo si bloccherà e attenderà l'input; il secondo uscirà quando EOF viene ricevuto dal excomando, quindi lo script continuerà. Esistono soluzioni alternative, ad esempio s///e, ma non sono specificate da POSIX. Preferisco usare il modulo portatile, che è mostrato sopra.

Per il gcomando, ci deve essere una nuova riga alla fine e preferisco usare printfper avvolgere i comandi piuttosto che incorporare una nuova riga tra virgolette singole.


1
C'è un motivo per cui stai usando printfe non echoo qualcosa del genere ex -c COMMAND?
David Foerster,

@DavidFoerster, sì. Ho iniziato a risponderti nei commenti ma è cresciuto a lungo, quindi l'ho aggiunto alla risposta.
Wildcard

Grazie e +1! Conoscevo printfvs. echo(anche se di solito preferisco solo echoquando l'argomento è hard-coded) ma non ho usato exampiamente finora.
David Foerster,

2

Ecco un paio di opzioni usando perl.

Dato che stai abbinando un solo carattere, puoi utilizzare tr/C//(una traduzione, senza sostituzioni), per restituire il numero di corrispondenze di C:

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

Più in generale, se si desidera abbinare una stringa di più caratteri o un'espressione regolare, è possibile utilizzare questo:

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

Ciò assegna le corrispondenze dell'espressione regolare /C/ga un elenco @me stampa le linee quando la lunghezza di tale elenco non lo è 1.

Lo -iswitch può essere aggiunto per modificare "sul posto".


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'

Si noti che presuppone GNU sed, in t #...genere si dirama verso l'etichetta chiamata #...nella maggior parte delle altre sedimplementazioni.
Stéphane Chazelas,

Anche !bis is GNU sed poiché a branch non piace nulla tranne un'etichetta o una nuova riga dopo di essa.

Sì, b, t, :, }(e r file, w file...) non può avere un comando dopo di loro sulla stessa linea. Puoi anche usare -eopzioni separate .
Stéphane Chazelas,

L'opzione perl non produce l'output corretto. Immagino che ti sia dimenticato di aggiungere il gmodificatore.
Tom Fenech,

@TomFenech Hai ragione. Lo sto risolvendo. Grazie.

1

Per chiunque volesse awkspecificamente, offrirei

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

salta la linea se corrisponde al motivo, stampalo altrimenti. In realtà non è necessario {print}, è possibile utilizzare //e stampare in modo predefinito, ma penso che sia più chiaro.

Il mio primo pensiero è stato quello di utilizzare egrep -vcon lo stesso modello, ma in realtà non risponde alla domanda come posta.


1
Qual è il punto di abbinare qualcosa dopo {next}? Basta dire awk '/pattern/ {next} 1'e verranno stampate tutte le linee che non corrispondono al modello. O, meglio, awk '!/pattern/'per stampare direttamente quelli.
fedorqui,

@fedorqui ha un buon punto !/pattern/(che in qualche modo mi è sfuggito di mente) ma preferirei di gran lunga vedere un autoesplicativo //{print}che un enigmatico 1. Assumi la minima competenza e fluidità dalla persona successiva per mantenere il tuo codice, in linea con il non renderlo seriamente meno efficiente o efficace.
nigel222,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.