¿Cómo reemplazar solo la enésima aparición de un patrón en un archivo?

10

Cómo reemplazar la tercera aparición de la cadena en el archivo usando el sedcomando.

Ejemplo:

Cambie solo la tercera aparición de isa usen el archivo.

Mi archivo de entrada contiene:

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

Espero que la salida sea:

hai this is linux.
hai thus is unix.
hai this is mac.
hai this is unchanged.
Suresh Kumar
fuente
3
La entrada y la salida son iguales.
Hauke ​​Laging
44
sedNo es la herramienta adecuada para el trabajo.
choroba
@don_crissti lo arreglé. El OP no había utilizado las herramientas de formato (por cierto, Sureshkumar, consulte aquí para obtener ayuda sobre la edición de sus preguntas) y los editores sucesivos han entendido mal lo que se quería.
terdon

Respuestas:

11

Es mucho más fácil hacerlo con perl.

Para cambiar la 3 rd ocurrencia:

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

Para cambiar cada aparición:

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

Cuando la cadena de reemplazo se produce solo una vez por línea, puede combinar diferentes utilidades.
Cuando la entrada está en el archivo "input" y está reemplazando "is" por "us", puede usar

LINENR=$(cat input | grep -n " is " | head -3 | tail -1 | cut -d: -f1)
cat input | sed ${LINENR}' s/ is / us /'
Walter A
fuente
En el ejemplo de la pregunta, hay más de uno ispor línea.
terdon
Pensé que estabas buscando "es" con espacios. Podría editar mi respuesta con el comando tr como @jimmij usó, pero mi solución se volvería mucho más inferida que la suya.
Walter A
No soy el autor de la pregunta :). Yo pensaba lo mismo, por lo que me había upvoted su respuesta, pero si nos fijamos en la versión original de la pregunta (Haga clic en la "X minutos Editado hace" link) verá que el PO espera que el es en este para ser cambiado a así . Por cierto, no hay necesidad de gato allí.
terdon
2

El siguiente script (que usa la sintaxis de GNU sed ) se puede usar para la edición in situ, no para la salida porque detiene las líneas de impresión después de la sustitución deseada:

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

Si te gusta la decisión de choroba , puedes modificarla arriba para

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

que genera todas las líneas

O debe colocar todas las líneas en el espacio del patrón (en la memoria, así que tenga cuidado con la limitación de tamaño) y realice la sustitución

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

Puede usar sedpara eso si anteriormente las nuevas líneas se reemplazan por cualquier otro carácter, por ejemplo:

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

Y lo mismo con puro (GNU) sed:

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

( sedreemplazo de nueva línea robado descaradamente de /programming//a/1252191/4488514 )

jimmij
fuente
Si vas a usar GNU sedsintaxis específica, que también podría utilizar sed -z 's/is/us/3'.
Stéphane Chazelas
@ StéphaneChazelas -zdebe ser una característica completamente nueva, mi GNU sed version 4.2.1no sabe nada acerca de esta opción.
jimmij
1
Añadido en 4.2.2 (2012). En su segunda solución, no necesita la conversión a \x0paso.
Stéphane Chazelas
Perdón por la edición. No había visto la versión original de la pregunta y alguien la había entendido mal y editó la línea incorrecta. Regresé a la versión 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'

Esa parte sedsimplemente lleva un recuento de isocurrencias de una línea a la siguiente. Debe manejar de manera confiable tantos ises por línea como le arroje, y no necesita almacenar las líneas antiguas mientras lo hace; solo retiene un solo carácter de nueva línea por cada iscosa que encuentra, que no es parte de otra palabra.

El resultado es que modificará solo la tercera aparición en un archivo, y llevará los recuentos por línea. Entonces, si un archivo se ve así:

1. is is isis
2. is does

... se imprimirá ...

1. is is isis
2. us does

Primero maneja los casos de borde insertando un espacio en la cabeza y la cola de cada línea. Esto hace que los límites de las palabras sean un poco más fáciles de determinar.

A continuación, busca valores válidos isinsertando una \nlínea de e antes de que todas las apariciones isque preceden inmediatamente a cero o un signo de puntuación seguido de un espacio. Hace otra pasada y elimina todos los \newlines que están precedidos inmediatamente por un carácter que no es espacio. Estos marcadores dejados coincidirán is.y isno, thiso no ?is.

A continuación, reúne cada marcador en la cola de la cadena: para cada \nicoincidencia en una línea, agrega una \nlínea de flecha a la cola de la cadena y la reemplaza con io u. Si hay 3 \newlines en una fila reunida en la cola de la cadena, entonces usa la u, de lo contrario, la i. La primera vez que se usa au también es la última: el reemplazo activa un bucle infinito que se reduce a get line, print line, get line, print line,y así sucesivamente.

Al final de cada ciclo de bucle de prueba, limpia los espacios insertados, imprime solo hasta la primera línea nueva en el espacio del patrón y vuelve a funcionar.

Agregaré un lcomando ook en la parte superior del bucle como:

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

... y eche un vistazo a lo que hace, ya que funciona con esta entrada:

hai this is linux.
hai this is unix.


hai this is mac.
hai this is unchanged is.

... así que esto es lo que hace:

 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

Tiene más sentido quizás con más ises por línea:

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'
)        

Eso es prácticamente lo mismo, pero escrito con POSIX BRE y manejo de argumentos rudimentarios.

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

... consigue ...

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

... y si habilito ${dbg}:

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

... podemos verlo 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
fuente
¿Te diste cuenta de que tu ejemplo dice "isis"?
flarn2006
@ flarn2006: estoy bastante seguro de que dice que sí.
mikeserv
0

Aquí hay una solución lógica que usa sedy trdebe escribirse en un script para que funcione. El siguiente código reemplaza cada tercera aparición de la palabra especificada en el sedcomando. Reemplace i=3con i=npara que esto funcione para cualquiera 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 qué esto funciona:

Supongamos que el archivo de texto es a b b b b a c a d a b b b a b e b z b s b a b.

  • Cuando n = 2: queremos reemplazar cada segunda aparición 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
      . . ^ . ^ . . . . . . ^ . . ^ . . . ^ . ^ . ^
    • Primero reemplazamos la segunda aparición, luego la tercera aparición, luego la cuarta, quinta, etc. Cuente en la secuencia que se muestra arriba para ver esto por sí mismo.
  • Cuando n = 3: queremos reemplazar cada tercera aparición 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
      . . . ^ . . . . . . . ^ . . . . ^ . . . . . ^
    • Primero reemplazamos la tercera ocurrencia, luego la quinta, luego la séptima, novena, undécima, y ​​así sucesivamente.
  • Cuando n = 4: queremos reemplazar cada tercera aparición de b.

    • Primero reemplazamos la cuarta ocurrencia, luego la séptima, luego la décima, la decimotercera y así sucesivamente.
agdhruv
fuente