Como remover uma linha se ela contiver um caractere exatamente uma vez

10

Quero remover uma linha de um arquivo que contém um caractere específico apenas uma vez, se ele estiver presente mais de uma vez ou não estiver presente, mantenha a linha no arquivo.

Por exemplo:

DTHGTY
FGTHDC
HYTRHD
HTCCYD
JUTDYC

Aqui, o caractere que eu quero remover é Cassim, o comando deve remover as linhas FGTHDCe JUTDYCporque elas têm Cexatamente uma vez.

Como posso fazer isso usando sedou awk?

Namz
fonte

Respostas:

20

Em awkvocê pode definir o separador de campos para qualquer coisa. Se você configurá-lo como C, terá tantos campos +1 como ocorrências de C.

Então, se você diz awk -F'C' '{print NF}' <<< "C1C2C3"que recebe 4: CCCconsiste em 3 Cs e, portanto, em 4 campos.

Você deseja remover as linhas nas quais Cocorre exatamente uma vez. Levando isso em consideração, no seu caso, você desejará remover as linhas nas quais existem exatamente dois Ccampos. Então, pule-os:

$ awk -F'C' 'NF!=2' file
DTHGTY
HYTRHD
HTCCYD
fedorqui
fonte
4
Uso astuto do awkseparador de campo!
Valentin B.
interessante, como no caso padrão (FS = ""), ele ignora espaços à esquerda ($ 1 = o primeiro não espaço na linha) e também repetições (você pode ter 5 espaços para separar o campo 1 e o campo 2) ... space provavelmente é tratado especialmente? (ver, pode-se fazer awk 'BEGIN { print "FS={" FS"}","OFS={" OFS "}";} {printf "%d fields : ",NF; for (i=1;i<=NF;i++) {printf "{" $i "} ";}; print "" }'e alimentá-lo com algumas linhas, alguns com múltiplos spces, e outros begininng com espaço (s))
Olivier Dulac
2
@OlivierDulac, sim, o espaço é tratado especialmente como especificado pelo POSIX .
Curinga
8

abordagem sed :

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

-i opção permite modificação de arquivo no local

/^[^C]*C[^C]*$/- corresponde a linhas que contêm Capenas uma vez

d - excluir linhas correspondentes

RomanPerekhrest
fonte
8

Isso pode ser feito com sed:

Código:

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

Resultados:

DTHGTY
HYTRHD
HTCCYD

Quão?

  1. Combine e imprima qualquer linha com pelo menos duas cópias de Cvia/C.*C/p
  2. Exclua qualquer linha com uma Cvia /C/d, isso inclui as linhas já impressas na etapa 1
  3. Padrão imprime o restante das linhas
Stephen Rauch
fonte
2
Abordagem alternativa inteligente; Eu gosto disso.
Curinga
6

Isso remove as linhas com exatamente uma ocorrência de C.

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

A expressão regular [^C]corresponde a um caractere que não é C (ou nova linha) e o operador de repetição (também conhecido como estrela de Kleene) *especifica zero ou mais repetições da expressão anterior.

A saída padrão de grep(e a maioria das outras ferramentas orientadas a texto) é a saída padrão; redirecione para um novo arquivo e, talvez, mova-o sobre o arquivo original, se é isso que você deseja. O mesmo regex pode ser usado com sed -ia edição no local:

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

(Em algumas plataformas, especialmente * BSD, incluindo macOS, a -iopção requer um argumento, como -i ''.)

triplo
fonte
1
sed -i '/^[^C]*C[^C]*$/d' file- parece que foi publicado antes, como você acha plágio?
RomanPerekhrest
1
De fato, há alguma duplicação. Comecei com a grepresposta, mas obviamente ela se estende facilmente à sed -ivariante. Não encontrou sua resposta porque estava procurando por greprespostas anteriores .
Tripleee
1
É mais seguro para apenas claramente evitar -icom sede em vez de redirecionamento para um novo arquivo e substituir o original com que se a sedutilidade saiu com nenhum erro.
Kusalananda
2
Ougrep -vx '[^C]*C[^C]*'
Stéphane Chazelas
@ Kusalananda Mas você também pode usar grepporque é mais claro e robusto (em particular, sedpossui um código de saída menos informativo).
Tripleee
4

A ferramenta POSIX para edições com script de um arquivo (em vez de imprimir o conteúdo modificado para o padrão) é ex.

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

Claro que você pode usarsed -i se a sua versão do Sed suportar, apenas lembre-se de que não é portátil se você estiver escrevendo um script destinado a ser executado em diferentes tipos de sistemas.


David Foerster perguntou nos comentários:

Existe uma razão pela qual você está usando printfe não echoou algo parecido ex -c COMMAND?

Resposta: Sim.

Para printfvs. echoé uma questão de portabilidade; consulte Por que printf é melhor que eco? E também é mais fácil intercalar novas linhas entre comandos usando printf.

Para printf ... | exvs. ex -c ..., é uma questão de tratamento de erros. Para este comando específico, isso não importaria, mas em geral importa; por exemplo, tente colocar

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

em um script. Contraste com o seguinte:

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

O primeiro travará e aguardará entrada; o segundo sairá quando o EOF for recebido pelo excomando, portanto, o script continuará. Existem soluções alternativas, como s///e, mas elas não são especificadas pelo POSIX. Eu prefiro usar o formulário portátil, que é mostrado acima.

Para o gcomando, deve haver uma nova linha no final, e eu prefiro usar printfpara agrupar os comandos do que incorporar uma nova linha entre aspas simples.

Curinga
fonte
1
Existe uma razão pela qual você está usando printfe não echoou algo parecido ex -c COMMAND?
David Foerster
@DavidFoerster, sim. Comecei a responder em comentários, mas ele demorou muito, então adicionei à resposta.
Curinga
Obrigado e +1! Eu sabia sobre printfvs. echo(embora normalmente prefira apenas echoquando o argumento é codificado), mas ainda não o usei exextensivamente.
David Foerster
2

Aqui estão algumas opções usando perl.

Como você está correspondendo apenas a um único caractere, você pode usar tr/C//(uma tradução, sem substituições), para retornar o número de correspondências de C:

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

De maneira mais geral, se você deseja corresponder uma sequência de vários caracteres ou expressão regular, pode usar o seguinte:

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

Isso atribui as correspondências da expressão regular /C/ga uma lista @me imprime linhas quando o comprimento dessa lista não é 1.

A -iopção pode ser adicionada para editar "no local".

Tom Fenech
fonte
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'

fonte
Note-se que ele assume GNU sed, t #...normalmente desviar para o rótulo chamado #...na maioria das outras sedimplementações.
Stéphane Chazelas
Até o !bGNU sed já que branch não gosta de nada, exceto um rótulo ou uma nova linha depois dele.
Sim, b, t, :, }(e r file, w file...) não pode ter um comando depois deles na mesma linha. Você também pode usar -eopções separadas .
Stéphane Chazelas
Sua opção perl não produz a saída correta. Eu acho que você esqueceu de adicionar o gmodificador.
Tom Fenech
@ TomFenech Você está correto. Eu estou consertando isso. Obrigado.
1

Para quem desejar awkespecificamente, eu oferecer

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

pule a linha se corresponder ao padrão, imprima-o de outra forma. Na verdade {print}, você não precisa , pode usar uma //impressão padrão, mas acho que é mais claro.

Meu primeiro pensamento foi usar egrep -vo mesmo padrão, mas isso não responde à pergunta como foi colocada.

nigel222
fonte
1
Qual é o sentido de combinar qualquer coisa depois {next}? Basta dizer awk '/pattern/ {next} 1'e todas as linhas não correspondentes ao padrão serão impressos. Ou, melhor, awk '!/pattern/'imprimi-los diretamente.
Fedorqui 2/17
@fedorqui bom argumento sobre !/pattern/(o que de alguma forma escorregou na minha mente), mas eu preferiria ver um auto-explicativo do //{print}que um enigmático 1. Assuma o mínimo de competência e fluência da próxima pessoa para manter seu código, consistente em não torná-lo menos eficiente ou eficaz.
Nigel222