Partido no codicioso con SED regex (emular perl's. *?)

22

Quiero usar sedpara reemplazar cualquier cosa en una cadena entre la primera ABy la primera aparición de AC(inclusive) con XXX.

Por ejemplo , tengo esta cadena (esta cadena es solo para una prueba):

ssABteAstACABnnACss

y me gustaría una salida similar a la siguiente: ssXXXABnnACss.


Hice esto con perl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

pero quiero implementarlo con sed. Lo siguiente (usando la expresión regular compatible con Perl) no funciona:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss
بارپابابا
fuente
2
Esto no tiene sentido. Tienes una solución de trabajo en Perl, pero quieres usar Sed, ¿por qué?
Kusalananda

Respuestas:

16

Las expresiones regulares de Sed coinciden con el partido más largo. Sed no tiene equivalente a no codicioso.

Obviamente lo que queremos hacer es unir

  1. AB,
    seguido de
  2. cualquier cantidad de cualquier cosa que no sea AC,
    seguido de
  3. AC

Desafortunadamente, sedno puedo hacer el n. ° 2, al menos no para una expresión regular de varios caracteres. Por supuesto, para una expresión regular de un solo carácter como @(o incluso [123]), podemos hacer [^@]*o [^123]*. Por esto se puede evitar limitaciones de sed, cambiando todas las apariciones de ACa @y luego la búsqueda de

  1. AB,
    seguido de
  2. cualquier número de cualquier cosa que no sea @,
    seguido de
  3. @

Me gusta esto:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

La última parte cambia instancias inigualables de @regreso a AC.

Pero, por supuesto, este es un enfoque imprudente, ya que la entrada ya podría contener @caracteres, por lo que, al unirlos, podríamos obtener falsos positivos. Sin embargo, dado que ninguna variable de shell tendrá nunca un carácter NUL ( \x00), es probable que NUL sea un buen carácter para usar en la solución anterior en lugar de @:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

El uso de NUL requiere GNU sed. (Para asegurarse de que las características de GNU estén habilitadas, el usuario no debe haber configurado la variable de shell POSIXLY_CORRECT).

Si está utilizando sed con el -zindicador de GNU para manejar la entrada separada por NUL, como la salida de find ... -print0, entonces NUL no estará en el espacio del patrón y NUL es una buena opción para la sustitución aquí.

Aunque NUL no puede estar en una variable bash, es posible incluirlo en un printfcomando. Si su cadena de entrada puede contener cualquier carácter, incluido NUL, consulte la respuesta de Stéphane Chazelas que agrega un método de escape inteligente.

John1024
fuente
Acabo de editar su respuesta para agregar una larga explicación; siéntase libre de cortarlo o enrollarlo.
G-Man dice 'reinstalar a Monica' el
@ G-Man ¡Esa es una excelente explicación! Muy bien hecho. Gracias.
John1024
Puede echoo printfun `\ 000 'bien en bash (o la entrada podría provenir de un archivo). Pero, en general, una cadena de texto probablemente no tenga NUL.
ilkkachu
@ilkkachu Tienes razón en eso. Lo que debería haber escrito es que ninguna variable o parámetro de shell puede contener NUL. Respuesta actualizada
John1024
¿No sería esto mucho más seguro si cambiaras ACde AC@nuevo?
Michael Vehrs
7

Algunas sedimplementaciones tienen soporte para eso. ssedtiene un modo PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT&T ast sed tiene conjunción y negación cuando se usan expresiones regulares aumentadas :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

Portablemente, puede usar esta técnica: reemplace la cadena final (aquí AC) con un solo carácter que no aparece en la cadena inicial o final (como :aquí) para que pueda hacerlo s/AB[^:]*://, y en caso de que ese carácter pueda aparecer en la entrada , utilice un mecanismo de escape que no entre en conflicto con las cadenas de inicio y fin.

Un ejemplo:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

Con GNU sed, un enfoque es utilizar la nueva línea como el personaje de reemplazo. Debido a que sedprocesa una línea a la vez, la nueva línea nunca ocurre en el espacio del patrón, por lo que se puede hacer:

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

Eso generalmente no funciona con otras sedimplementaciones porque no son compatibles [^\n]. Con GNU seddebe asegurarse de que la compatibilidad POSIX no esté habilitada (como con la variable de entorno POSIXLY_CORRECT).

Stéphane Chazelas
fuente
6

No, las expresiones regulares sed no tienen coincidencias no codiciosas.

Puede hacer coincidir todo el texto hasta la primera aparición ACutilizando "cualquier cosa que no contenga AC" seguido de AC, que hace lo mismo que Perl .*?AC. La cuestión es que "cualquier cosa que no contenga AC" no se puede expresar fácilmente como una expresión regular: siempre hay una expresión regular que reconoce la negación de una expresión regular, pero la expresión regular de la negación se complica rápidamente. Y en sed portátil, esto no es posible en absoluto, porque la negación regex requiere agrupar una alternancia que está presente en expresiones regulares extendidas (por ejemplo, en awk) pero no en expresiones regulares básicas portátiles. Algunas versiones de sed, como GNU sed, tienen extensiones de BRE que permiten expresar todas las expresiones regulares posibles.

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

Debido a la dificultad de negar una expresión regular, esto no se generaliza bien. Lo que puede hacer en su lugar es transformar la línea temporalmente. En algunas implementaciones de sed, puede usar nuevas líneas como marcador, ya que no pueden aparecer en una línea de entrada (y si necesita varios marcadores, use nueva línea seguida de un carácter variable).

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

Sin embargo, tenga en cuenta que la barra diagonal inversa no funciona en un juego de caracteres con algunas versiones sed. En particular, esto no funciona en GNU sed, que es la implementación de sed en Linux no incrustado; en GNU sed puedes usar \nen su lugar:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

En este caso específico, es suficiente reemplazar el primero ACpor una nueva línea. El enfoque que presenté anteriormente es más general.

Un enfoque más poderoso en sed es guardar la línea en el espacio de espera, eliminar todo excepto la primera parte "interesante" de la línea, intercambiar el espacio de espera y el espacio de patrón o agregar el espacio de patrón al espacio de espera y repetir. Sin embargo, si comienza a hacer cosas que son tan complicadas, realmente debería pensar en cambiar a awk. Awk tampoco tiene una coincidencia no codiciosa, pero puede dividir una cadena y guardar las partes en variables.

Gilles 'SO- deja de ser malvado'
fuente
@ilkkachu No, no lo hace. s/\n//gelimina todas las líneas nuevas.
Gilles 'SO- deja de ser malvado'
asdf. Bien, mi mal.
ilkkachu
3

sed - correspondencia no codiciosa por Christoph Sieghart

El truco para obtener coincidencias no codiciosas en sed es hacer coincidir todos los caracteres, excepto el que termina la coincidencia. Lo sé, es obvio, pero perdí unos minutos preciosos y los scripts de shell deberían ser, después de todo, rápidos y fáciles. Entonces, en caso de que alguien más lo necesite:

Emparejamiento codicioso

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

Emparejamiento no codicioso

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar

gresolio
fuente
3
El término "obvio" es ambiguo. En este caso, no está claro que usted (o Christoph Sieghart) hayan pensado esto. En particular, hubiera sido bueno si hubieras mostrado cómo resolver el problema específico en la pregunta (donde el cero de más expresión es seguido por más de un carácter ) . Puede encontrar que esta respuesta no funciona bien en ese caso.
Scott
La madriguera del conejo es mucho más profunda de lo que me pareció a primera vista. Tiene razón, esa solución alternativa no funciona bien para la expresión regular de varios caracteres.
gresolio
0

En su caso, puede negar el cierre de char de esta manera:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'
midori
fuente
2
La pregunta dice: "Quiero reemplazar cualquier cosa entre la primera ABy la primera aparición de ACcon XXX...", y da ssABteAstACABnnACsscomo ejemplo de entrada. Esta respuesta funciona para ese ejemplo , pero no responde la pregunta en general. Por ejemplo, ssABteCstACABnnACsstambién debería producir el resultado aaXXXABnnACss, pero su comando pasa esta línea sin cambios.
G-Man dice 'reinstalar a Monica' el
0

La solución es bastante simple. .*es codicioso, pero no es absolutamente codicioso. Considera hacer coincidir ssABteAstACABnnACsscontra la expresión regular AB.*AC. Lo ACque sigue .*debe tener una coincidencia. El problema es que debido a que .*es codicioso, el siguiente ACcoincidirá con el último en AC lugar del primero. .*se come el primero ACmientras que el literal ACen la expresión regular coincide con el último en ssABteAstACABnn AC ss. Para evitar que esto suceda, simplemente reemplace el primero ACcon algo ridículo para diferenciarlo del segundo y de cualquier otra cosa.

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

El codicioso .*ahora se detendrá al pie de -foobar-adentro ssABteAst-foobar-ABnnACssporque no hay otro -foobar-que esto -foobar-, y la expresión regular -foobar- DEBE tener una coincidencia. El problema anterior era que la expresión regular ACtenía dos coincidencias, pero como .*era codicioso, ACse seleccionó la última coincidencia . Sin embargo, con -foobar-solo una coincidencia es posible, y esta coincidencia demuestra que .*no es absolutamente codicioso. La parada de autobús para .*ocurre donde solo queda un partido para el resto de la expresión regular siguiente .*.

Tenga en cuenta que esta solución fallará si ACaparece una antes de la primera ABporque ACse reemplazará la incorrecta -foobar-. Por ejemplo, después de la primera sedsustitución, se ACssABteAstACABnnACssconvierte en -foobar-ssABteAstACABnnACss; por lo tanto, no se puede encontrar una coincidencia en contra AB.*-foobar-. Sin embargo, si la secuencia es siempre ... AB ... AC ... AB ... AC ..., entonces esta solución tendrá éxito.

JD Graham
fuente
0

Una alternativa es cambiar la cadena para que desee la coincidencia codiciosa

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

Use revpara revertir la cadena, revierta sus criterios de coincidencia, use sedde la manera habitual y luego revierta el resultado ...

ssAB-+-+-+-+ACABnnACss
bu5hman
fuente