Como substituir apenas a enésima ocorrência de um padrão em um arquivo?

10

Como substituir a terceira ocorrência da sequência no arquivo usando o sedcomando

Exemplo:

Altere apenas a terceira ocorrência de ispara usno arquivo.

Meu arquivo de entrada contém:

hai this is linux.
hai this is unix.
hai this is mac.
hai this is unchanged.

Estou esperando saída é:

hai this is linux.
hai thus is unix.
hai this is mac.
hai this is unchanged.
Sureshkumar
fonte
3
Entrada e saída são iguais.
Hauke ​​Laging
4
sednão é a ferramenta certa para o trabalho.
choroba
@don_crissti Eu o consertei. O OP não usou as ferramentas de formatação (a propósito, Sureshkumar, veja aqui para obter ajuda na edição de suas perguntas) e editores sucessivos entenderam mal o que era desejado.
terdon

Respostas:

11

É muito mais fácil fazer isso perl.

Para alterar o 3 rd ocorrência:

perl -pe 's{is}{++$n == 3 ? "us" : $&}ge'

Para alterar a cada ocorrência:

perl -pe 's{is}{++$n % 3 ? $& : "us"}ge'
Stéphane Chazelas
fonte
3

Quando a sequência de substituição ocorre apenas uma vez por linha, você pode combinar diferentes utilitários.
Quando a entrada está no arquivo "input" e você está substituindo "is" por "us", você pode usar

LINENR=$(cat input | grep -n " is " | head -3 | tail -1 | cut -d: -f1)
cat input | sed ${LINENR}' s/ is / us /'
Walter A
fonte
No exemplo da pergunta, há mais de um ispor linha.
terdon
Eu pensei que você estava procurando "é" com espaços. Eu poderia editar minha resposta com o comando tr como @jimmij usado, mas minha solução se tornaria muito inferior à dele.
Walter A
Eu não sou o autor da pergunta :). Eu pensei a mesma coisa, que é por isso que eu tinha upvoted sua resposta, mas se você olhar para a versão original da pergunta (clique em "Editado X minutos atrás" link), você verá que o OP esperava que o é no presente para ser alterado para assim . By the way, não há necessidade de gato lá.
terdon
2

O script abaixo (usando a sintaxe GNU sed ) é utilizável para edição no local e não para saída, pois interrompe as linhas de impressão após a substituição desejada:

sed -i '/is/{: 1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; q}' text.file

Se sua decisão como choroba você pode modificar acima para

sed '/is/{:1 ; /\(.*is\)\{3\}/!{N;b1} ; s/is/us/3 ; :2 ; n ; $!b2}' text.file

que gera todas as linhas

Ou você deve colocar todas as linhas no espaço do padrão (na memória, tenha cuidado com a limitação de tamanho) e faça a substituição

sed ': 1 ; N ; $!b1 ; s/is/us/3 ' text.file
Costas
fonte
2

Você pode usar sedisso se anteriormente as novas linhas forem substituídas por outros caracteres, por exemplo:

tr '\n' '\000' | sed 's/is/us/3' | tr '\000' '\n'

E o mesmo com puro (GNU) sed:

sed ':a;N;$!ba;s/\n/\x0/g;s/is/us/3;s/\x0/\n/g'

( sedsubstituição da nova linha roubada descaradamente de /programming//a/1252191/4488514 )

jimmij
fonte
Se você usará a sedsintaxe específica do GNU , poderá usá-lo sed -z 's/is/us/3'.
Stéphane Chazelas
@ StéphaneChazelas -zdeve ser um novo recurso, o meu GNU sed version 4.2.1não sabe nada sobre essa opção.
jimmij
1
Adicionado em 4.2.2 (2012). Na sua segunda solução, você não precisa da conversão para a \x0etapa.
Stéphane Chazelas
Desculpe pela edição. Eu não tinha visto a versão original da pergunta e alguém a entendeu mal e editou a linha errada. Eu voltei para a versão anterior.
terdon
1
p='[:punct:]' s='[:space:]'
sed -Ee'1!{/\n/!b' -e\}            \
     -e's/(\n*)(.*)/ \2 \1/'       \
     -e"s/is[$p]?[$s]/\n&/g"       \
     -e"s/([^$s])\n/\1/g;1G"       \
-e:c -e"s/\ni(.* )\n{3}/u\1/"      \
     -e"/\n$/!s/\n//g;/\ni/G"      \
     -e's//i/;//tc'                \
     -e's/^ (.*) /\1/;P;$d;N;D'

Essa parte sedcarrega apenas um registro de isocorrências de uma linha para a seguinte. Ele deve manipular confiavelmente o número de ises por linha que você lançar nela, e não precisa armazenar em buffer as linhas antigas enquanto o faz - apenas retém um único caractere de nova linha para cada um isque encontrar que não faça parte de outra palavra.

O resultado é que ele modificará apenas a terceira ocorrência em um arquivo - e contará por linha. Portanto, se um arquivo se parecer com:

1. is is isis
2. is does

... será impresso ...

1. is is isis
2. us does

Primeiro ele lida com casos de borda, inserindo um espaço na cabeça e no final de cada linha. Isso facilita a verificação dos limites das palavras.

A seguir, procura ises válidas inserindo uma \nlinha de e-mail antes que todas as ocorrências isprecedam imediatamente zero ou um caractere de pontuação, seguidos por um espaço. Ele faz outra passagem e remove todas as \nlinhas de ew que são imediatamente precedidas por um caractere não espaço. Esses marcadores deixados para trás corresponderão is.e ismas não thisou ?is.

Em seguida, ele reúne cada marcador \nino final da sequência - para cada partida em uma linha, ele anexa uma linha de \new ao final da sequência e o substitui por um iou u. Se houver três \nlinhas de linha seguidas no final da corda, ele usará o u - senão o i. A primeira vez que au é usada também é a última - a substituição desencadeia um loop infinito que se resume a get line, print line, get line, print line,e assim por diante.

No final de cada ciclo de loop de tentativa, limpa os espaços inseridos, imprime apenas até a primeira nova linha que ocorrer no espaço do padrão e continua novamente.

Vou adicionar um lcomando ook na cabeça do loop, como:

l; s/\ni(.* )\n{9}/u\1/...

... e dê uma olhada no que faz, pois funciona com esta entrada:

hai this is linux.
hai this is unix.


hai this is mac.
hai this is unchanged is.

... então aqui está o que ele faz:

 hai this \nis linux. \n$        #behind the scenes
hai this is linux.               #actually printed
 hai this \nis unix. \n\n$       #it builds the marker string
hai this is unix.
  \n\n\n$                        #only for lines matching the

  \n\n\n$                        #pattern - and not otherwise.

 hai this \nis mac. \n\n\n$      #here's the match - 3 ises so far in file.
hai this us mac.                 #printed
hai this is unchanged is.        #no look here - this line is never evaled

Faz mais sentido, talvez com mais ises por linha:

nthword()(  p='[:punct:]' s='[:space:]'         
    sed -e '1!{/\n/!b' -e\}             \
        -e 's/\(\n*\)\(.*\)/ \2 \1/'    \
        -e "s/$1[$p]\{0,1\}[$s]/\n&/g"  \
        -e "s/\([^$s]\)\n/\1/g;1G;:c"   \
        -e "${dbg+l;}s/\n$1\(.* \)\n\{$3\}/$2\1/" \
        -e '/\n$/!s/\n//g;/\n'"$1/G"    \
        -e "s//$1/;//tc" -e 's/^ \(.*\) /\1/'     \
        -e 'P;$d;N;D'
)        

Isso é praticamente a mesma coisa, mas escrito com POSIX BRE e manipulação rudimentar de argumentos.

 printf 'is is. is? this is%.0s\n' {1..4}  | nthword is us 12

... fica ...

is is. is? this is
is is. is? this is
is is. is? this us
is is. is? this is

... e se eu ativar ${dbg}:

printf 'is is. is? this is%.0s\n' {1..4}  | 
dbg=1 nthword is us 12

... podemos assistir iterar ...

 \nis \nis. \nis? this \nis \n$
 is \nis. \nis? this \nis \n\n$
 is is. \nis? this \nis \n\n\n$
 is is. is? this \nis \n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n$
is is. is? this is
 \nis \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n$
 is \nis. \nis? this \nis \n\n\n\n\n\n\n\n\n\n$
 is is. \nis? this \nis \n\n\n\n\n\n\n\n\n\n\n$
 is is. is? this \nis \n\n\n\n\n\n\n\n\n\n\n\n$
is is. is? this us
is is. is? this is
mikeserv
fonte
Você percebeu que seu exemplo diz "isis"?
precisa
@ flarn2006 - eu tenho certeza que ele diz que é.
mikeserv
0

Aqui está uma solução lógica que usa sede trdeve ser escrita em um script para que funcione. O código abaixo substitui cada terceira ocorrência da palavra especificada no sedcomando. Substitua i=3por i=npara fazer isso funcionar para qualquer um n.

Código:

# replace new lines with '^' character to get everything onto a single line
tr '\n' '^' < input.txt > output.txt

# count number of occurrences of the word to be replaced
num=`grep -o "apple" "output.txt" | wc -l`

# in successive iterations, replace the i + (n-1)th occurrence
n=3
i=3
while [ $i -le $num ]
do
    sed -i '' "s/apple/lemon/${i}" 'output.txt'
    i=$(( i + (n-1) ))
done

# replace the '^' back to new line character
tr '^' '\n' < output.txt > tmp && mv tmp output.txt


Por que isso funciona:

Suponha que o arquivo de texto seja a b b b b a c a d a b b b a b e b z b s b a b.

  • Quando n = 2: queremos substituir cada segundo ocorrência de b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . ^ . ^ . . . . . . ^ . . ^ . . . ^ . ^ . ^
    • Primeiro substituímos a 2ª ocorrência, depois a 3ª ocorrência, depois a 4ª, 5ª e assim por diante. Conte na sequência mostrada acima para ver por si mesmo.
  • Quando n = 3: queremos substituir cada terceira ocorrência de b.

    • a b b b b a c a d a b b b a b e b z b s b a b
      . . . ^ . . . . . . . ^ . . . . ^ . . . . . ^
    • Primeiro substituímos a 3ª ocorrência, depois a 5ª, depois a 7ª, 9ª, 11ª e assim por diante.
  • Quando n = 4: queremos substituir cada terceira ocorrência de b.

    • Primeiro, substituímos a quarta ocorrência, depois a sétima, depois a décima, a décima terceira e assim por diante.
agdhruv
fonte