¿Es posible escapar metacaracteres de expresiones regulares de forma confiable con sed

Me pregunto si es posible escribir un comando sed 100% confiable para escapar de los metacaracteres regex en una cadena de entrada para que se pueda usar en un comando sed posterior. Me gusta esto:

 #!/bin/bash # Trying to replace one regex by another in an input file with sed search="/abc\n\t[az]\+\([^ ]\)\{2,3\}\3" replace="/xyz\n\t[0-9]\+\([^ ]\)\{2,3\}\3" # Sanitize input search=$(sed 'script to escape' <<< "$search") replace=$(sed 'script to escape' <<< "$replace") # Use it in a sed command sed "s/$search/$replace/" input 

Sé que hay mejores herramientas para trabajar con cadenas fijas en lugar de patrones, por ejemplo awk , perl o python . Me gustaría probar si es posible o no con sed . ¡Diría que concentrémonos en expresiones regulares de POSIX para divertirnos aún más! 🙂

He intentado muchas cosas, pero en cualquier momento pude encontrar una entrada que rompió mi bash. Pensé que mantenerlo abstracto como un script to escape no llevaría a nadie en la dirección equivocada.

Por cierto, la discusión surgió aquí . Pensé que este podría ser un buen lugar para recostackr soluciones y probablemente romperlas y / o elaborarlas.

Nota:

  • Si está buscando una funcionalidad preempaquetada basada en las técnicas discutidas en esta respuesta:
    • bash funciones bash que permiten un escape robusto incluso en sustituciones multilínea se pueden encontrar en la parte inferior de esta publicación (más una solución perl que usa el soporte integrado de perl para tal escape).
    • La respuesta de @MedMorton contiene una herramienta (script bash ) que realiza de manera robusta sustituciones de línea única .
  • Todos los fragmentos asumen bash como el shell (son posibles las reformulaciones que cumplen con POSIX):

SOLUCIONES de línea única


Escapar un literal de cadena para usar como expresión regular en sed :

Para dar crédito donde se debe crédito: encontré la expresión regular que se usa a continuación en esta respuesta .

Suponiendo que la cadena de búsqueda es una cadena de línea simple :

 search='abc\n\t[az]\+\([^ ]\)\{2,3\}\3' # sample input containing metachars. searchEscaped=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<<"$search") # escape it. sed -n "s/$searchEscaped/foo/p" <<<"$search" # if ok, echoes 'foo' 
  • Cada carácter excepto ^ se coloca en su propia expresión de conjunto de caracteres [...] para tratarlo como un literal.
    • Tenga en cuenta que ^ es el único char. no puede representar como [^] , porque tiene un significado especial en esa ubicación (negación).
  • Entonces, ^ caracteres. se escapan como \^ .

El enfoque es robusto, pero no eficiente.

La robustez proviene de no tratar de anticipar todos los caracteres especiales de expresiones regulares , que varían en los dialectos de expresiones regulares, sino de centrarse en solo 2 características compartidas por todos los dialectos de expresiones regulares :

  • la capacidad de especificar caracteres literales dentro de un conjunto de caracteres.
  • la capacidad de escapar de un literal ^ como \^

Escapar un literal de cadena para utilizar como cadena de reemplazo en el comando s/// sed :

La cadena de reemplazo en un comando sed s/// no es una expresión regular, pero reconoce marcadores de posición que hacen referencia a toda la cadena que coincide con la expresión regular ( & ) o los resultados específicos del grupo de captura por índice ( \1 , \2 ..), entonces deben escaparse, junto con el delimitador de expresiones regulares (habitual), / .

Suponiendo que la cadena de reemplazo es una cadena de línea simple :

 replace='Laurel & Hardy; PS\2' # sample input containing metachars. replaceEscaped=$(sed 's/[&/\]/\\&/g' <<<"$replace") # escape it sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" # if ok, outputs $replace as is 


Soluciones MULTI-line


Escapar un literal de cadena MULTI-LINE para usar como expresión regular en sed :

Nota : Esto solo tiene sentido si se han leído varias líneas de entrada (posiblemente TODAS) antes de intentar hacer coincidir.
Dado que las herramientas como sed y awk operan en una sola línea a la vez de manera predeterminada, se necesitan pasos adicionales para hacer que lean más de una línea a la vez.

 # Define sample multi-line literal. search='/abc\n\t[az]\+\([^ ]\)\{2,3\}\3 /def\n\t[AZ]\+\([^ ]\)\{3,4\}\4' # Escape it. searchEscaped=$(sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$search" | tr -d '\n') #' # Use in a Sed command that reads ALL input lines up front. # If ok, echoes 'foo' sed -n -e ':a' -e '$!{N;ba' -e '}' -e "s/$searchEscaped/foo/p" <<<"$search" 
  • Las líneas nuevas en cadenas de entrada de varias líneas se deben traducir a '\n' cadenas , que es cómo se codifican las líneas nuevas en una expresión regular.
  • $!a\'$'\n''\\n' añade la cadena '\n' a cada línea de salida, pero la última (se ignora la última nueva línea, porque fue agregada por <<< )
  • tr -d '\n luego elimina todas las nuevas líneas reales de la cadena ( sed agrega una cada vez que imprime su espacio de patrón), reemplazando efectivamente todas las líneas nuevas en la entrada con '\n' cadenas.
  • -e ':a' -e '$!{N;ba' -e '}' es la forma compatible con POSIX de un idioma sed que lee todas las líneas de entrada en un bucle, por lo tanto, deja comandos posteriores para operar en todas las líneas de entrada en una vez.

    • Si está utilizando GNU sed (solo), puede usar su opción -z para simplificar la lectura de todas las líneas de entrada a la vez:
      sed -z "s/$searchEscaped/foo/" <<<"$search"

Escapar un literal de cadena MULTI-LINE para usar como la cadena de reemplazo en el comando s/// sed :

 # Define sample multi-line literal. replace='Laurel & Hardy; PS\2 Masters\1 & Johnson\2' # Escape it for use as a Sed replacement string. IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace") replaceEscaped=${REPLY%$'\n'} # If ok, outputs $replace as is. sed -n "s/\(.*\) \(.*\)/$replaceEscaped/p" <<<"foo bar" 
  • Las líneas nuevas en la cadena de entrada se deben conservar como nuevas líneas reales, pero \ -escaped.
  • -e ':a' -e '$!{N;ba' -e '}' es la forma compatible con POSIX de un idioma sed que lee todas las líneas de entrada en un bucle.
  • 's/[&/\]/\\&/g escapa a todas las instancias & , \ e / , como en la solución de una sola línea.
  • s/\n/\\&/g' luego \ -prefixes todas las nuevas líneas reales.
  • IFS= read -d '' -r se usa para leer la salida del comando sed como está (para evitar la eliminación automática de nuevas líneas finales que una sustitución de comando ( $(...) ) llevaría a cabo).
  • ${REPLY%$'\n'} luego elimina una sola nueva línea final, que el <<< ha anexado implícitamente a la entrada.


funciones bash basadas en lo anterior (para sed ):

  • quoteRe() comillas (escapes) para usar en una expresión regular
  • quoteSubst() comillas para usar en la cadena de sustitución de una llamada s/// .
  • ambos manejan la entrada multilínea correctamente
    • Tenga en cuenta que dado que sed lee una sola línea a la vez de manera predeterminada, el uso de quoteRe() con cadenas de varias líneas solo tiene sentido en los comandos sed que explícitamente leen varias (o todas) líneas a la vez.
    • Además, el uso de sustituciones de comando ( $(...) ) para llamar a las funciones no funcionará para las cadenas que tienen nuevas líneas al final ; en ese caso, use algo como IFS= read -d '' -r escapedValue <(quoteSubst "$value")
 # SYNOPSIS # quoteRe  quoteRe() { sed -e 's/[^^]/[&]/g; s/\^/\\^/g; $!a\'$'\n''\\n' <<<"$1" | tr -d '\n'; } 
 # SYNOPSIS # quoteSubst  quoteSubst() { IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$1") printf %s "${REPLY%$'\n'}" } 

Ejemplo:

 from=$'Cost\(*):\n$3.' # sample input containing metachars. to='You & I'$'\n''eating A\1 sauce.' # sample replacement string with metachars. # Should print the unmodified value of $to sed -e ':a' -e '$!{N;ba' -e '}' -e "s/$(quoteRe "$from")/$(quoteSubst "$to")/" <<<"$from" 

Tenga en cuenta el uso de -e ':a' -e '$!{N;ba' -e '}' para leer todas las entradas a la vez, de modo que la sustitución de líneas múltiples funcione.



solución perl :

Perl tiene soporte integrado para escapar cadenas arbitrarias para uso literal en una expresión regular: la función quotemeta() o su equivalente \Q...\E citando .
El enfoque es el mismo para cadenas de una o varias líneas; por ejemplo:

 from=$'Cost\(*):\n$3.' # sample input containing metachars. to='You owe me $1/$& for'$'\n''eating A\1 sauce.' # sample replacement string w/ metachars. # Should print the unmodified value of $to. # Note that the replacement value needs NO escaping. perl -s -0777 -pe 's/\Q$from\E/$to/' -- -from="$from" -to="$to" <<<"$from" 
  • Tenga en cuenta el uso de -0777 para leer todas las entradas a la vez, de modo que la sustitución de líneas múltiples funcione.

  • La opción -s permite colocar -= -style Definiciones de variables de Perl a continuación -- después de la secuencia de comandos, antes de que cualquier nombre de archivo opere.

Basándose en la respuesta de @mklement0 en este hilo, la siguiente herramienta reemplazará cualquier cadena de una sola línea (a diferencia de regexp) con cualquier otra cadena de una sola línea usando sed y bash :

 $ cat sedstr #!/bin/bash old="$1" new="$2" file="${3:--}" escOld=$(sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$old") escNew=$(sed 's/[&/\]/\\&/g' <<< "$new") sed "s/$escOld/$escNew/g" "$file" 

Para ilustrar la necesidad de esta herramienta, intente reemplazar a.*/b{2,}\nc con d&e\1f llamando directamente a sed :

 $ cat file a.*/b{2,}\nc axx/bb\nc $ sed 's/a.*/b{2,}\nc/d&e\1f/' file sed: -e expression #1, char 16: unknown option to `s' $ sed 's/a.*\/b{2,}\nc/d&e\1f/' file sed: -e expression #1, char 23: invalid reference \1 on `s' command's RHS $ sed 's/a.*\/b{2,}\nc/d&e\\1f/' file a.*/b{2,}\nc axx/bb\nc # .... and so on, peeling the onion ad nauseum until: $ sed 's/a\.\*\/b{2,}\\nc/d\&e\\1f/' file d&e\1f axx/bb\nc 

o usa la herramienta anterior:

 $ sedstr 'a.*/b{2,}\nc' 'd&e\1f' file d&e\1f axx/bb\nc 

La razón por la cual esto es útil es que se puede boost fácilmente para usar los delimitadores de palabras para reemplazar las palabras si es necesario, por ejemplo, en la syntax de GNU sed :

 sed "s/\<$escOld\>/$escNew/g" "$file" 

mientras que las herramientas que realmente operan en cadenas (por ejemplo, el index() awk index() ) no pueden usar separadores de palabras.