Sed - Reemplaza las primeras k instancias de una palabra en el archivo

24

Quiero reemplazar solo las primeras kinstancias de una palabra.

¿Cómo puedo hacer esto?

P.ej. Dicho archivo foo.txtcontiene 100 instancias de la palabra 'linux'.

Necesito reemplazar las primeras 50 ocurrencias solamente.

narendra-choudhary
fuente
1
Puede consultar esto: unix.stackexchange.com/questions/21178/…
cuonglm el
¿Necesita sed específicamente, o son aceptables otras herramientas? ¿Necesita trabajar en la línea de comandos o es aceptable un editor de texto?
evilsoup
Cualquier cosa que funcione en la línea de comando es aceptable.
narendra-choudhary

Respuestas:

31

La primera sección a continuación describe el uso sedpara cambiar las primeras k-ocurrencias en una línea. La segunda sección amplía este enfoque para cambiar solo las primeras k-ocurrencias en un archivo, independientemente de en qué línea aparezcan.

Solución orientada a líneas

Con sed estándar, hay un comando para reemplazar la k-ésima aparición de una palabra en una línea. Si kes 3, por ejemplo:

sed 's/old/new/3'

O bien, uno puede reemplazar todas las ocurrencias con:

sed 's/old/new/g'

Ninguno de estos es lo que quieres.

GNU sedofrece una extensión que cambiará la k-ésima ocurrencia y todo después de eso. Si k es 3, por ejemplo:

sed 's/old/new/g3'

Estos se pueden combinar para hacer lo que quieras. Para cambiar las 3 primeras ocurrencias:

$ 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

donde \nes útil aquí porque podemos estar seguros de que nunca ocurre en una línea.

Explicación:

Utilizamos tres sedcomandos de sustitución:

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

    Esta es la extensión GNU para reemplazar el cuarto y todos los sucesos posteriores de oldcon \n.

    La función de expresión regular extendida \<se usa para hacer coincidir el comienzo de una palabra y \>para hacer coincidir el final de una palabra. Esto asegura que solo las palabras completas coincidan. La expresión regular extendida requiere la -Eopción sed.

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

    Solo quedan las tres primeras ocurrencias oldy esto las reemplaza a todas new.

  • s/\n/old/g

    El cuarto y todos los sucesos restantes de oldfueron reemplazados por \nen el primer paso. Esto los devuelve a su estado original.

Solución no GNU

Si GNU sed no está disponible y desea cambiar las primeras 3 apariciones de olda new, utilice tres 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

Esto funciona bien cuando kes un número pequeño pero se escala deficiente a grande k.

Dado que algunos seds que no son GNU no admiten la combinación de comandos con punto y coma, cada comando aquí se presenta con su propia -eopción. También puede ser necesario verificar que sedadmite los símbolos de límite de palabras, \<y \>.

Solución orientada a archivos

Podemos decirle a sed que lea todo el archivo y luego realice las sustituciones. Por ejemplo, para reemplazar las tres primeras ocurrencias del olduso de un sed de estilo BSD:

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

Los comandos sed H;1h;$!d;xleen todo el archivo.

Como lo anterior no utiliza ninguna extensión GNU, debería funcionar en sed BSD (OSX). Tenga en cuenta, pensó, que este enfoque requiere un sedque puede manejar largas colas. GNU seddebería estar bien. Aquellos que usan una versión de GNU no seddeben probar su capacidad para manejar largas colas.

Con un GNU sed, podemos usar el gtruco descrito anteriormente, pero con \nreemplazado por \x00, para reemplazar los primeros tres casos:

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

Este enfoque se escala bien y se khace grande. Sin embargo, esto \x00supone que no está en su cadena original. Dado que es imposible poner el carácter \x00en una cadena bash, esto generalmente es una suposición segura.

John1024
fuente
55
Esto solo funciona para líneas y cambiará las primeras 4 ocurrencias en cada línea
1
@mikeserv Excelente idea! Respuesta actualizada
John1024
(1) Menciona sed GNU y no GNU, y sugiere tr '\n' '|' < input_file | sed …. Pero, por supuesto, eso convierte toda la entrada en una línea, y algunos seds que no son GNU no pueden manejar líneas arbitrariamente largas. (2) Usted dice: "... arriba, la cadena entre comillas '|'debe reemplazarse por cualquier carácter, o cadena de caracteres, ..." Pero no puede usar trpara reemplazar un carácter con una cadena (de longitud> 1). (3) En tu último ejemplo, dices -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/new. Esto parece ser un error tipográfico para -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'.
G-Man dice 'reinstalar a Monica' el
@ G-Man Muchas gracias! He actualizado la respuesta.
John1024
esto es tan feo
Louis Maddox
8

Usando Awk

Los comandos awk se pueden usar para reemplazar las primeras N apariciones de la palabra con el reemplazo.
Los comandos solo reemplazarán si la palabra es una coincidencia completa.

En los ejemplos a continuación, estoy reemplazando las primeras 27apariciones de oldconnew

Usando sub

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

Este comando recorre cada campo hasta que coincide old, comprueba que el contador está por debajo de 27, aumenta y sustituye la primera coincidencia en la línea. Luego se mueve al siguiente campo / línea y se repite.

Reemplazar el campo manualmente

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

Similar al comando anterior pero como ya tiene un marcador en qué campo depende ($i), simplemente cambia el valor del campo de olda new.

Realizar un chequeo antes

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

Comprobar que la línea contiene elementos viejos y que el contador está por debajo de 27 SHOULDproporciona un pequeño aumento de velocidad, ya que no procesará líneas cuando sean falsas.

RESULTADOS

P.ej

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

a

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
fuente
El primero (usando sub) hace lo incorrecto si la cadena "old" precede a la palabra * old; por ejemplo, “Dale un poco de oro para el anciano.” → “Dale un poco GNEW al anciano.”
G-hombre dice 'Restablecer Mónica'
@ G-Man Sí, olvidé el $ibit, ha sido editado, gracias :)
7

Supongamos que desea reemplazar solo las primeras tres instancias de una cadena ...

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: lo anterior probablemente no funcionará con comentarios incrustados
... o en mi caso de ejemplo, de un '1' ...

SALIDA:

22
211
211
311

Allí uso dos técnicas notables. En primer lugar, cada aparición de 1una línea se reemplaza por \n1. De esta manera, como hago los reemplazos recursivos a continuación, puedo estar seguro de no reemplazar la ocurrencia dos veces si mi cadena de reemplazo contiene mi cadena de reemplazo. Por ejemplo, si lo reemplazo hecon heyél, aún funcionará.

Hago esto como:

s/1/\
&/g

En segundo lugar, estoy contando los reemplazos agregando un personaje al hespacio antiguo para cada ocurrencia. Una vez que llegue a tres, no ocurrirán más. Si aplica esto a sus datos y cambia los \{3\}reemplazos totales que desea y las /\n1/direcciones a lo que quiera reemplazar, debe reemplazar solo los que desee.

Solo hice todas las -ecosas para facilitar la lectura. POSIXly Podría escribirse así:

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

Y con GNU sed:

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

Recuerde también que sedestá orientado a líneas: no se lee en todo el archivo y luego intenta volver a recorrerlo, como suele ser el caso en otros editores. sedEs simple y eficiente. Dicho esto, a menudo es conveniente hacer algo como lo siguiente:

Aquí hay una pequeña función de shell que lo agrupa en un comando simplemente ejecutado:

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

Entonces con eso puedo hacer:

seq 11 100 311 | firstn 7 1 5

...y obten...

55
555
255
311

...o...

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

...Llegar...

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

... o, para que coincida con su ejemplo (en un orden de magnitud 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
fuente
4

Una alternativa corta en Perl:

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

Cambie el valor de `$ n $ a su gusto.

Cómo funciona:

  • Para cada línea, se sigue tratando de sustituir newpor old( s/old/new/) y siempre que se pueda, se incrementa la variable $i( ++$i).
  • Sigue trabajando en la línea ( 1 while ...) siempre y cuando haya realizado menos $nsustituciones en total y puede realizar al menos una sustitución en esa línea.
Joseph R.
fuente
4

Use un bucle de concha y ex!

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

Sí, es un poco tonto.

;)

Nota: Esto puede fallar si hay menos de 50 instancias olden el archivo. (No lo he probado). Si es así, dejaría el archivo sin modificar.


Mejor aún, usa Vim.

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

Explicación:

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
Comodín
fuente
: s // new <CR> debería funcionar también, porque una expresión regular vacía reutiliza la última búsqueda utilizada
aproximadamente
3

Una solución simple pero no muy rápida es recorrer los comandos descritos en /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -archivo

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

Este comando sed en particular probablemente solo funcione para GNU sed y si newword no es parte de oldword . Para sed no GNU, vea aquí cómo reemplazar solo el primer patrón en un archivo.

jofel
fuente
+1 para identificar que reemplazar "viejo" con "negrita" puede causar problemas.
G-Man dice 'reinstalar a Monica' el
2

Con GNU awkpuede establecer el separador de registros RSpara la palabra que se reemplazará delimitada por límites de palabras. Entonces se trata de establecer el separador de registros en la salida a la palabra de reemplazo para los primeros kregistros mientras se conserva el separador de registros original para el resto

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

O

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