Sed - Substitua primeiro k instâncias de uma palavra no arquivo

20

Eu quero substituir apenas as primeiras k instâncias de uma palavra.

Como posso fazer isso?

Digamos que o arquivo foo.txt contenha 100 instâncias ocorrências da palavra 'linux'.

Eu preciso substituir apenas 50 ocorrências.

    
por narendra-choudhary 16.09.2014 / 07:34

7 respostas

24

A primeira seção descreve como usar sed para alterar as primeiras ocorrências k em uma linha. A segunda seção estende essa abordagem para alterar apenas as primeiras ocorrências k em um arquivo, independentemente de em qual linha elas aparecem.

Solução orientada por linha

Com sed padrão, existe um comando para substituir a k-ésima ocorrência de uma palavra em uma linha. Se k for 3, por exemplo:

sed 's/old/new/3'

Ou pode substituir todas as ocorrências por:

sed 's/old/new/g'

Nenhum destes é o que você quer.

O GNU sed oferece uma extensão que mudará a k-ésima ocorrência e depois disso. Se k é 3, por exemplo:

sed 's/old/new/g3'

Estes podem ser combinados para fazer o que você quiser. Para alterar as 3 primeiras ocorrências:

$ 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

onde \n é útil aqui porque podemos ter certeza de que nunca ocorre em uma linha.

Explicação:

Usamos três comandos de substituição sed :

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

    Esta é a extensão GNU para substituir a quarta e todas as ocorrências subseqüentes de old com \n .

    O recurso de regex estendido \< é usado para corresponder ao início de uma palavra e \> para corresponder ao final de uma palavra. Isso garante que apenas palavras completas sejam correspondidas. Regex estendido requer a opção -E para sed .

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

    Somente as três primeiras ocorrências de old permanecem e isso substitui todas elas por new .

  • s/\n/old/g

    A quarta e todas as ocorrências restantes de old foram substituídas por \n na primeira etapa. Isso os retorna de volta ao seu estado original.

Solução não-GNU

Se o GNU sed não estiver disponível e você quiser alterar as 3 primeiras ocorrências de old para new , use três comandos s :

$ 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

Isso funciona bem quando k é um número pequeno, mas não é muito adequado para k .

Como alguns seds não-GNU não suportam a combinação de comandos com ponto e vírgula, cada comando aqui é introduzido com sua própria opção -e . Também pode ser necessário verificar se o sed suporta os símbolos de limite de palavras, \< e \> .

Solução orientada a arquivos

Podemos dizer ao sed para ler todo o arquivo e depois realizar as substituições. Por exemplo, para substituir as três primeiras ocorrências de old usando um sed do tipo BSD:

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

Os comandos sed H;1h;$!d;x leem todo o arquivo.

Como o acima não usa nenhuma extensão GNU, ele deve funcionar no BSD (OSX) sed. Note, pensou, que esta abordagem requer um sed que pode lidar com linhas longas. O% GNUsed deve estar bem. Aqueles que usam uma versão não-GNU de sed devem testar sua capacidade de lidar com linhas longas.

Com um GNU sed, podemos usar o truque g descrito acima, mas com \n substituído por \x00 , para substituir as três primeiras ocorrências:

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

Essa abordagem é dimensionada bem quando k se torna grande. Isso pressupõe, no entanto, que \x00 não esteja em sua string original. Como é impossível colocar o caractere \x00 em uma string bash, isso geralmente é uma suposição segura.

    
por 16.09.2014 / 07:58
7

Digamos que você queira substituir apenas as três primeiras instâncias de uma string ...

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

nota: o texto acima provavelmente não funcionará com comentários incorporados
... ou no meu exemplo, de um '1' ...

OUTPUT:

22
211
211
311

Lá eu uso duas técnicas notáveis. Em primeiro lugar, toda ocorrência de 1 em uma linha é substituída por \n1 . Dessa forma, como faço as substituições recursivas a seguir, posso ter certeza de não substituir a ocorrência duas vezes se minha string de substituição contiver minha string de substituição. Por exemplo, se eu substituir he por hey , ainda funcionará.

Eu faço assim:

s/1/\
&/g

Em segundo lugar, estou contando as substituições adicionando um caractere a h old space para cada ocorrência. Quando eu chegar a três, não mais. Se você aplicar isso aos seus dados e alterar o \{3\} para o total de substituições desejado e os /\n1/ endereços para o que você pretende substituir, substitua apenas quantos desejar.

Eu só fiz todo o material -e para legibilidade. POSIXly Poderia ser escrito assim:

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

E com GNU sed :

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

Lembre-se também que sed é orientado à linha - ele não lê todo o arquivo e, em seguida, tenta fazer o loop novamente, como é frequentemente o caso em outros editores. sed é simples e eficiente. Dito isso, muitas vezes é conveniente fazer algo como o seguinte:

Aqui está uma pequena função de shell que é agrupada em um comando simplesmente executado:

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

Então, com isso eu posso fazer:

seq 11 100 311 | firstn 7 1 5

... e obtenha ...

55
555
255
311

... ou ...

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

... para obter ...

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

... ou, para corresponder ao seu exemplo (em uma ordem menor de magnitude) :

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
    
por 16.09.2014 / 21:34
7

Usando o Awk

Os comandos awk podem ser usados para substituir as primeiras N ocorrências da palavra pela substituição.
Os comandos só substituirão se a palavra for uma correspondência completa.

Nos exemplos abaixo, estou substituindo as primeiras 27 ocorrências de old por new

Usando sub

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

This command loops through each field until it matches old, it checks the counter is below 27, increments and the substitutes the first match on the line. Then moves onto the next field/line and repeats.

Substituindo o campo manualmente

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

Similar to the command before but as it already has a marker on which field it is up to ($i), it simply changes the value of the field from old to new.

Realizando uma verificação antes

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

Checking that the line contains old and the counter is below 27 SHOULD provide a small speed boost as it won't process lines when these are false.

RESULTADOS

Por exemplo

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

para

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
    
por 16.09.2014 / 11:10
3

Uma solução simples, mas não muito rápida, é executar os comandos descritos em   link

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

Este comando sed provavelmente funciona apenas para o GNU sed e se newword não faz parte de oldword . Para o não-GNU sed, consulte aqui como substituir apenas o primeiro padrão em um arquivo. p>     

por 16.09.2014 / 11:15
3

Uma alternativa curta em Perl:

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

Altere o valor de '$ n $ ao seu gosto.

Como funciona:

  • Para cada linha, ele tenta substituir new por old ( s/old/new/ ) e, sempre que pode, incrementa a variável $i ( ++$i ).
  • Ele continua trabalhando na linha ( 1 while ... ) desde que tenha feito menos de $n de substituições no total e possa fazer pelo menos uma substituição nessa linha.
por 17.09.2014 / 01:05
3

Use um loop de shell e ex !

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

Sim, é um pouco bobo.

;)

Nota: Isso pode falhar se houver menos de 50 instâncias de old no arquivo. (Eu não testei isso.) Em caso afirmativo, deixaria o arquivo inalterado.

Melhor ainda, use o Vim.

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

Explicação:

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
    
por 04.05.2016 / 04:46
2

Com o GNU awk você pode definir o separador de registro RS para a palavra a ser substituída delimitada por limites de palavras. Em seguida, é um caso de definir o separador de registro na saída para a palavra de substituição para os primeiros k registros, mantendo o separador de registro original para o restante

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

OR

awk -vRS='\ylinux\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
    
por 18.09.2014 / 14:41