Sed - Substitui as primeiras k instâncias de uma palavra no arquivo

24

Quero substituir apenas as primeiras kinstâncias de uma palavra.

Como posso fazer isso?

Por exemplo. O arquivo Say foo.txtcontém 100 ocorrências de instâncias da palavra 'linux'.

Preciso substituir apenas as 50 primeiras ocorrências.

narendra-choudhary
fonte
1
Você pode consultar isso: unix.stackexchange.com/questions/21178/…
cuonglm
Você precisa do sed especificamente ou outras ferramentas são aceitáveis? Você precisa trabalhar na linha de comando ou um editor de texto é aceitável?
evilsoup
Qualquer coisa que funcione na linha de comando é aceitável.
narendra-choudhary 16/09/14

Respostas:

31

A primeira seção a seguir descreve o uso sedpara alterar as primeiras k ocorrências em uma linha. A segunda seção estende essa abordagem para alterar apenas as primeiras k ocorrências em um arquivo, independentemente da linha em que elas aparecem.

Solução orientada a linhas

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

sed 's/old/new/3'

Ou, pode-se substituir todas as ocorrências por:

sed 's/old/new/g'

Nenhuma delas é o que você deseja.

O GNU sedoferece uma extensão que mudará a ocorrência de k-és e depois disso. Se k for 3, por exemplo:

sed 's/old/new/g3'

Estes podem ser combinados para fazer o que você deseja. 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 sedcomandos de substituição:

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

    Essa é a extensão GNU para substituir a quarta e todas as ocorrências subsequentes de oldcom \n.

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

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

    Apenas as três primeiras ocorrências de oldpermanecem e isso as substitui por todas new.

  • s/\n/old/g

    A quarta e todas as ocorrências restantes de oldforam substituídas por \nna primeira etapa. Isso os retorna ao seu estado original.

Solução não GNU

Se o GNU sed não estiver disponível e você desejar alterar as 3 primeiras ocorrências de oldpara new, use três scomandos:

$ 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 varia de mal a grande 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 -eopção. Também pode ser necessário verificar se você sedsuporta os símbolos de limite de palavras \<e \>.

Solução orientada a arquivos

Podemos dizer ao sed para ler o arquivo inteiro e depois executar as substituições. Por exemplo, para substituir as três primeiras ocorrências do olduso de um sed no estilo 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;xlêem o arquivo inteiro.

Como o descrito acima não usa nenhuma extensão GNU, ele deve funcionar no BSD (OSX) sed. Observe, pensou, que essa abordagem requer um sedque possa lidar com linhas longas. GNU seddeve estar bem. Aqueles que usam uma versão não-GNU seddevem testar sua capacidade de lidar com longas filas.

Com um GNU sed, podemos usar ainda mais o gtruque descrito acima, mas com \nsubstituí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 escala bem e kse torna grande. Isso pressupõe, porém, que \x00não esteja na sua string original. Como é impossível colocar o caractere \x00em uma string do bash, isso geralmente é uma suposição segura.

John1024
fonte
5
Isso funciona apenas para linhas e alterará as 4 primeiras ocorrências em cada linha
1
@mikeserv Excelente ideia! Resposta atualizada.
precisa saber é o seguinte
(1) Você menciona GNU e não GNU sed e sugere tr '\n' '|' < input_file | sed …. Mas, é claro, isso converte toda a entrada em uma linha, e alguns seds não-GNU não podem lidar com linhas arbitrariamente longas. (2) Você diz: “… acima, a cadeia de caracteres citada '|'deve ser substituída por qualquer caractere, ou cadeia de caracteres,…” Mas você não pode usar trpara substituir um caractere por uma cadeia de caracteres (de comprimento> 1). (3) No seu último exemplo, você diz -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Este parece ser um erro de digitação -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man Diz 'Reinstate Monica'
@ G-Man Muito obrigado! Eu atualizei a resposta.
precisa saber é o seguinte
isso é tão feio
Louis Maddox
8

Usando o Awk

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

Nos exemplos abaixo, estou substituindo as primeiras 27ocorrências de oldpornew

Usando sub

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

Esse comando percorre cada campo até corresponder old, verifica se o contador está abaixo de 27, incrementa e substitui a primeira correspondência na linha. Em seguida, passa para o próximo campo / linha e repete.

Substituindo o Campo Manualmente

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

Semelhante ao comando anterior, mas como ele já possui um marcador em qual campo ele depende ($i), ele simplesmente altera o valor do campo de oldpara new.

Executando uma verificação antes

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

A verificação de que a linha contém antiga e o contador está abaixo de 27 SHOULDfornecem um pequeno aumento de velocidade, pois não processa as linhas quando são falsas.

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
Jeff Schaller
fonte
O primeiro (usando sub) faz a coisa errada se a string "old" preceder a * palavra old; por exemplo, “Dê um pouco de ouro ao velho.” → “Dê um pouco de novidade ao velho.”
G-Man diz 'Reinstate Monica'
@ G-Man Sim, eu esqueci a $ipouco, tem sido editado, graças :)
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 acima provavelmente não funcionará com comentários incorporados
... ou, no meu exemplo, de um '1' ...

SAÍDA:

22
211
211
311

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

Eu faço assim:

s/1/\
&/g

Em segundo lugar, estou contando as substituições adicionando um caractere ao hespaço antigo para cada ocorrência. Quando chego a três, não ocorre mais. Se você aplicar isso aos seus dados e alterar as \{3\}substituições totais desejadas e os /\n1/endereços para o que você deseja substituir, substitua apenas o número que desejar.

Eu só fiz todas as -ecoisas para facilitar a leitura. 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 de que sedé orientado a linhas - ele não lê o arquivo inteiro e tenta repetir o processo, como costuma acontecer em outros editores. sedé simples e eficiente. Dito isto, muitas vezes é conveniente fazer algo como o seguinte:

Aqui está uma pequena função shell que agrupa 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 pegue...

55
555
255
311

...ou...

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

...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 de magnitude menor) :

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
mikeserv
fonte
4

Uma alternativa curta no 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 continua tentando substituir newpor old( s/old/new/) e sempre que pode, ele incrementa a variável $i( ++$i).
  • Ele continua trabalhando na linha ( 1 while ...) desde que tenha feito menos do que $nsubstituições no total e possa fazer pelo menos uma substituição nessa linha.
Joseph R.
fonte
4

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 pateta.

;)

Nota: Isso pode falhar se houver menos de 50 instâncias oldno arquivo. (Não testei.) Nesse caso, 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
Curinga
fonte
: s // new <CR> também deve funcionar, porque um regex vazio reutiliza a última pesquisa usada
eike
3

Uma solução simples, mas não muito rápida, é executar um loop sobre os comandos descritos em /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -Arquivo

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

Esse comando sed em particular provavelmente funciona apenas para o GNU sed e se newword não faz parte do oldword . Para sed não GNU, veja aqui como substituir apenas o primeiro padrão em um arquivo.

jofel
fonte
+1 para identificar que a substituição de "antigo" por "negrito" pode causar problemas.
G-Man Diz 'Reinstate Monica'
2

Com o GNU, awkvocê pode definir o separador de registros RScomo a palavra a ser substituída, delimitada pelos limites da palavra. É o caso de definir o separador de registros na saída como a palavra de substituição para os primeiros kregistros, mantendo o separador de registros original pelo restante

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

OU

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