Quero substituir apenas as primeiras k
instâncias de uma palavra.
Como posso fazer isso?
Por exemplo. O arquivo Say foo.txt
contém 100 ocorrências de instâncias da palavra 'linux'.
Preciso substituir apenas as 50 primeiras ocorrências.
Quero substituir apenas as primeiras k
instâncias de uma palavra.
Como posso fazer isso?
Por exemplo. O arquivo Say foo.txt
contém 100 ocorrências de instâncias da palavra 'linux'.
Preciso substituir apenas as 50 primeiras ocorrências.
Respostas:
A primeira seção a seguir descreve o uso sed
para 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.
Com o sed padrão, existe um comando para substituir a ocorrência de k-ésima de uma palavra em uma linha. Se k
for 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 sed
oferece 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.
Usamos três sed
comandos de substituição:
s/\<old\>/\n/g4
Essa é a extensão GNU para substituir a quarta e todas as ocorrências subsequentes de old
com \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 -E
opção de sed
.
s/\<old\>/new/g
Apenas as três primeiras ocorrências de old
permanecem e isso as substitui por todas 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 ao seu estado original.
Se o GNU sed não estiver disponível e você desejar alterar as 3 primeiras ocorrências de old
para new
, use três s
comandos:
$ 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 -e
opção. Também pode ser necessário verificar se você sed
suporta os símbolos de limite de palavras \<
e \>
.
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 old
uso 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;x
lê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 sed
que possa lidar com linhas longas. GNU sed
deve estar bem. Aqueles que usam uma versão não-GNU sed
devem testar sua capacidade de lidar com longas filas.
Com um GNU sed, podemos usar ainda mais o g
truque 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 escala bem e k
se torna grande. Isso pressupõe, porém, que \x00
não esteja na sua string original. Como é impossível colocar o caractere \x00
em uma string do bash, isso geralmente é uma suposição segura.
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 tr
para 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'
.
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 27
ocorrências de old
pornew
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 deold
paranew
.
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
SHOULD
fornecem 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
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' ...
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, 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 he
por hey
ele ainda funcionará.
Eu faço assim:
s/1/\
&/g
Em segundo lugar, estou contando as substituições adicionando um caractere ao h
espaç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 -e
coisas 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
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:
new
por old
( s/old/new/
) e sempre que pode, ele incrementa a variável $i
( ++$i
).1 while ...
) desde que tenha feito menos do que $n
substituições no total e possa fazer pelo menos uma substituição nessa linha.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 old
no 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
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.
Com o GNU, awk
você pode definir o separador de registros RS
como 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 k
registros, 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