¿Hay alguna manera de evitar que sed interprete la cadena de reemplazo? [cerrado]

14

Si desea reemplazar una palabra clave con una cadena usando sed, sed se esfuerza por interpretar su cadena de reemplazo. Si la cadena de reemplazo tiene caracteres que sed considera especiales, como un carácter '/', fallará, a menos que, por supuesto, haya querido decir que su cadena de reemplazo tiene caracteres que le dicen a sed cómo actuar.

Ex:

VAR="hi/"

sed "s/KEYWORD/$VAR/g" somefile

¿Hay alguna forma de decirle a sed que no intente interpretar la cadena de reemplazo para caracteres especiales? Todo lo que quiero es poder reemplazar una palabra clave en un archivo con el contenido de una variable, sin importar cuál sea ese contenido.

Tal
fuente
Si desea poner caracteres especiales sedy hacer que no sean especiales, simplemente barra diagonal inversa escapar de ellos. VAR='hi\/'No da tal problema.
Comodín el
66
¿Por qué todos los votos negativos? Me parece una pregunta perfectamente razonable
roaima
sed(1)solo interpreta lo que se pone. En su caso, lo obtiene a través de una interpolación de shell. Creo que no puede hacer lo que quiere, pero consulte el manual. Sé que en Perl (que hace un sedreemplazo pasable , con expresiones regulares mucho más ricas) puede especificar que una cadena se tome literalmente, nuevamente, consulte el manual.
vonbrand
stackoverflow.com/questions/407523/…
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Respuestas:

4

Sólo hay 4 caracteres especiales en la pieza de repuesto: \, &, nueva línea y el delimitador ( ref )

$ VAR='abc/def&ghi\foo
next line'

$ repl=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$VAR")

$ echo "$repl"
abc\/def\&ghi\\foo\
next line

$ echo ZYX | sed "s/Y/$repl/g"
Zabc/def&ghi\foo
next lineX
Glenn Jackman
fuente
Esto tiene el mismo problema que la solución de Antti: si la cadena de reemplazo supera una cierta longitud, aparece el error "Lista de argumentos demasiado larga". Además, ¿qué sucede si la cadena de reemplazo tiene '[', ']', '*', '.' Y otros caracteres similares? ¿Realmente no los interpretaría?
Tal
El lado de reemplazo de nos/// es una expresión regular, en realidad es solo una cadena (a excepción de las barras invertidas y los escapes ). Si la cadena de reemplazo es tan larga, un revestimiento de concha no es su solución. &
Glenn Jackman
Una lista muy útil si, por ejemplo, su cadena de reemplazo es texto codificado en base64 (por ejemplo, reemplazando un marcador de posición con una clave SHA256). Entonces es solo el delimitador de quien preocuparse.
Heath Raftery
4

Puede usar Perl en lugar de sed con -p(asumir el bucle sobre la entrada) y -e(dar el programa en la línea de comandos). Con Perl puede acceder a las variables de entorno sin interpolarlas en el shell. Tenga en cuenta que la variable debe exportarse :

export VAR='hi/'
perl -p -e 's/KEYWORD/$ENV{VAR}/g' somefile

Si no desea exportar la variable a todas partes, solo debe proporcionarla para ese proceso únicamente:

PATTERN="$VAR" perl -p -e 's/KEYWORD/$ENV{PATTERN}/g' somefile

Tenga en cuenta que la sintaxis de expresión regular de Perl es, por defecto, ligeramente diferente de la de sed.

Antti Haapala
fuente
Esto parecía muy prometedor, pero cuando lo pruebo, aparece el error "La lista de argumentos es demasiado larga" porque mi cadena de reemplazo es demasiado larga, lo cual tiene sentido: con este método, estamos usando la cadena de reemplazo completa como parte de los argumentos que damos perl, por lo que hay un límite sobre cuánto tiempo puede ser.
Tal
1
No, irá en la PATTERN variable de entorno , no en argumentos. En cualquier caso, este error sería E2BIG, que igualmente obtendría si lo usara sed.
Antti Haapala
2

La solución más simple que aún manejaría correctamente la gran mayoría de los valores de las variables sería utilizar un carácter que no se imprima como delimitador del sedcomando sustituto.

En vipuede escapar de cualquier carácter de control escribiendo Ctrl-V (más comúnmente escrito como ^V). Entonces, si usa algún carácter de control (a menudo lo uso ^Acomo delimitador en estos casos), su sedcomando solo se romperá si ese carácter no imprimible está presente en la variable que está colocando.

Entonces escribiría "s^V^AKEYWORD^V^A$VAR^V^Ag"y lo que obtendría (en vi) se vería así:

sed "s^AKEYWORD^A$VAR^Ag" somefile

Esto funcionará siempre $VARque no contenga el carácter que no se imprime, lo ^Acual es extremadamente improbable.


Por supuesto, si pasa la entrada del usuario al valor de $VAR, entonces todas las apuestas están desactivadas y será mejor que desinfecte su entrada a fondo en lugar de confiar en que los caracteres de control son difíciles de escribir para el usuario promedio.


Sin embargo, en realidad hay más de qué tener cuidado que la cadena delimitador. Por ejemplo, &cuando está presente en una cadena de reemplazo, significa "todo el texto que coincidió". Por ejemplo, s/stu../my&/reemplazaría "cosas" con "mystuff", "picado" con "mystung", etc. Entonces, si tiene algún carácter en la variable que está colocando como una cadena de reemplazo, pero desea usar el literal solo el valor de la variable, entonces tiene que desinfectar algunos datos antes de poder usar la variable como una cadena de reemplazo sed. (Sin sedembargo, la desinfección de datos también se puede hacer ).

Comodín
fuente
Ese es mi punto: reemplazar una cadena por otra cadena es una operación muy simple. ¿Realmente tiene que ser tan complicado como descubrir qué caracteres no le gustará y usar sed para desinfectar su propia entrada? Eso suena ridícula e innecesariamente complicado. No soy un programador profesional, pero estoy bastante seguro de que puedo codificar una pequeña función que reemplaza una palabra clave con una cadena en casi cualquier idioma que haya conocido, incluido bash: solo esperaba un Linux simple solución usando herramientas existentes: no puedo creer que no haya una disponible.
Tal
1
@Tal, si su cadena de reemplazo tiene "100 páginas de páginas" como usted menciona en otro comentario ... difícilmente puede llamarlo un caso de uso "simple". La respuesta aquí es Perl, por cierto, simplemente no he aprendido a Perl. La complejidad aquí proviene del hecho de que desea permitir CUALQUIER entrada arbitraria como una cadena de reemplazo en una expresión regular .
Comodín el
Hay muchas otras soluciones que podría usar, muchas de ellas muy simples. Por ejemplo, si su cadena de reemplazo está realmente basada en líneas y no necesita ser insertada en el medio de una línea, use sedel icomando nsert. Pero sedno es una buena herramienta para procesar grandes cantidades de texto de formas complejas. Publicaré otra respuesta que muestre cómo hacer esto awk.
Comodín el
1

En su lugar, puede usar a ,o a |y lo tomará como un separador y técnicamente podría usar cualquier cosa

desde la página del manual

\cregexpc
           Match lines matching the regular expression regexp.  The  c  may
      be any character.

Como puede ver, debe comenzar con un \ antes de su separador al principio, luego puede usarlo como separador.

de la documentación http://www.gnu.org/software/sed/manual/sed.html#The-_0022s_0022-Command :

The / characters may be uniformly replaced by any other single character 
within any given s command.

The / character (or whatever other character is used in its stead) can appear in 
the regexp or replacement only if it is preceded by a \ character.

Ejemplo:

sed -e 'somevar|s|foo|bar|'
echo "Hello all" | sed "s_all_user_"
echo "Hello all" | sed "s,all,user,"

echo "Hello/ World" | sed "s,Hello/,Neo,"

usuario3566929
fuente
Está hablando de permitir el uso de un único carácter específico en la cadena de reemplazo, en este caso, "/". Estoy hablando de evitar que intente interpretar la cadena de reemplazo por completo. No importa qué carácter use ("/", ",", "|", etc.) siempre corre el riesgo de que ese carácter aparezca en la cadena de reemplazo. Además, el carácter inicial no es el único carácter especial que le importa, ¿verdad?
Tal
@Tal no, puede tomar cualquier cosa en lugar de /e ignorará /felizmente como acabo de señalar ... de hecho, incluso puedes buscarlo y reemplazarlo en una cadena >>> he editado con un ejemplo >>> estos las cosas no son tan seguras y siempre encontrarás un tipo más inteligente
user3566929
@Tal, ¿por qué quieres evitar que interprete? Quiero decir que es el uso de, seden primer lugar, ¿cuál es su proyecto?
user3566929
Todo lo que necesito es reemplazar una palabra clave con una cadena. sed parece ser la forma más común, con mucho, de hacer esto en Linux. La cadena puede tener 100 páginas. No quiero tratar de desinfectar la cadena para que sed no se asuste al leerla; quiero que sea capaz de manejar cualquier carácter de la cadena, y con "manejar", quiero decir, no intentar encontrar magia significado dentro.
Tal
1
@Tal, NObash es para manipulación de cadenas. En absoluto, en absoluto, en absoluto. Es para la manipulación de archivos y la coordinación de comandos . Resulta que tiene algunas funciones prácticas incorporadas para cadenas, pero realmente limitadas y no muy rápidas si eso es lo principal que está haciendo. Consulte "¿Por qué usar un bucle de shell para procesar texto se considera una mala práctica?" Algunas herramientas que están diseñadas para el procesamiento de texto son, en orden de la más básica a la más poderosa , y Perl. sedawk
Comodín el
1

Si se basa en una línea y solo se debe reemplazar una línea, recomiendo anteponer el archivo con la línea de reemplazo usando printf, almacenar esa primera línea en sedel espacio de espera y soltarla según sea necesario. De esta manera, no tiene que preocuparse por caracteres especiales en absoluto. (La única suposición aquí es que $VARcontiene una sola línea de texto sin líneas nuevas, que es lo que ya dijo en los comentarios). Además de las líneas nuevas, VAR podría contener cualquier cosa y esto funcionaría independientemente.

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/KEYWORD/g'

printf '%s\n'imprimirá el contenido de $VARcomo una cadena literal, independientemente de su contenido, seguido de una nueva línea. ( echoen algunos casos, hará otras cosas, por ejemplo, si el contenido de $VARcomienza con un guión, se interpretará como un indicador de opción al que se pasa echo).

Las llaves se usan para anteponer la salida de printflos contenidos de a somefilemedida que se pasan sed. El espacio en blanco que separa las llaves por sí mismos es importante aquí, al igual que el punto y coma antes de la llave de cierre.

1{h;d;};como sedcomando almacenará la primera línea de texto en sedel espacio de espera y luego delegirá la línea (en lugar de imprimirla).

/KEYWORD/aplica las siguientes acciones a todas las líneas que contienen KEYWORD. La acción es get, que obtiene el contenido del espacio de espera y lo coloca en lugar del espacio del patrón; en otras palabras, la línea actual completa. (Esto no es para reemplazar solo una parte de una línea.) El espacio de espera no se vacía, por cierto, solo se copia en el espacio del patrón, reemplazando lo que esté allí.

Si desea anclar su expresión regular para que no coincida con una línea que simplemente contiene KEYWORD, sino solo una línea donde no hay nada más en la línea que KEYWORD, agregue un ancla de inicio de línea ( ^) y un ancla de final de línea ( $) a tu expresión regular:

VAR=whatever
{ printf '%s\n' "$VAR";cat somefile; } | sed '1{h;d;};/^KEYWORD$/g'
Comodín
fuente
Parece genial si su VAR tiene una línea de largo. De hecho, mencioné en los comentarios que VAR "puede tener 100 páginas" en lugar de una sola línea. Perdón por la confusion.
Tal
0

Puede hacer una barra diagonal inversa para escapar de las barras diagonales en su cadena de reemplazo, utilizando la expansión del parámetro de sustitución de patrones de Bash. Es un poco desordenado porque las barras diagonales también deben escaparse para Bash.

$ var='a/b/c';var="${var//\//\\/}";echo 'this is a test' | sed "s/i/$var/g"

salida

tha/b/cs a/b/cs a test

Usted podría poner la expansión de parámetros directamente en su comando sed:

$ var='a/b/c';echo 'this is a test' | sed "s/i/${var//\//\\/}/g"

pero creo que la primera forma es un poco más legible. Y, por supuesto, si va a reutilizar el mismo patrón de reemplazo en múltiples comandos sed, tiene sentido hacer la conversión una vez.

Otra opción sería usar un script escrito en awk, perl o Python, o un programa en C, para hacer sus sustituciones en lugar de usar sed.


Aquí hay un ejemplo simple en Python que funciona si la palabra clave que se va a reemplazar es una línea completa en el archivo de entrada (sin contar la nueva línea). Como puede ver, es esencialmente el mismo algoritmo que su ejemplo de Bash, pero lee el archivo de entrada de manera más eficiente.

import sys

#Get the keyword and replacement texts from the command line
keyword, replacement = sys.argv[1:]
for line in sys.stdin:
    #Strip any trailing whitespace
    line = line.rstrip()
    if line == keyword:
        line = replacement
    print(line)
PM 2Ring
fuente
Esta es solo otra forma de desinfectar la entrada, y no es una excelente, ya que solo maneja un carácter específico ('/'). Como Wildcard señaló, hay más de lo que tener cuidado que solo la cadena delimitador.
Tal
Llamada justa Por ejemplo, si el texto de reemplazo contiene alguna secuencia con barra invertida, se interpretará, lo que puede no ser deseable. Una forma de evitarlo sería convertir los caracteres problemáticos (o todo) en \xsecuencias de escape de estilo. O para usar un programa que pueda manejar entradas arbitrarias, como mencioné en mi último párrafo.
PM 2Ring
@Tal: Agregaré un ejemplo simple de Python a mi respuesta.
PM 2Ring
El script de Python funciona muy bien y parece hacer exactamente lo que hace mi función, solo que de manera mucho más eficiente. Desafortunadamente, si la secuencia de comandos principal es bash (como es mi caso), esto requiere el uso de una secuencia de comandos externa de Python.
Tal
-1

Así es como fui:

#Replaces a keyword with a long string
#
#This is normally done with sed, but sed
#tries to interpret the string you are
#replacing the keyword with too hard
#
#stdin - contents to look through
#Arg 1 - keyword to replace
#Arg 2 - what to replace keyword with
replace() {
        KEYWORD="$1"
        REPLACEMENT_STRING="$2"

        while IFS= read -r LINE
        do
                if [[ "$LINE" == "$KEYWORD" ]]
                then
                        printf "%s\n" "$REPLACEMENT_STRING"
                else
                        printf "%s\n" "$LINE"
                fi
        done < /dev/stdin
}

esto funciona muy bien en mi caso porque mi palabra clave está en una línea por sí sola. Si la palabra clave estuviera en una línea con otro texto, esto no funcionaría.

Todavía me gustaría saber si hay una manera fácil de hacer esto que no implique codificar mi propia solución.

Tal
fuente
1
Si realmente te preocupan los caracteres especiales y la robustez, no deberías usarlos echoen absoluto. Usar en su printflugar. Y hacer procesamiento de texto en un bucle de shell es una mala idea.
Comodín el
1
Hubiera sido útil si mencionara en la pregunta que la palabra clave siempre será una línea completa. FWIW, bash reades bastante lento. Está destinado a procesar la entrada interactiva del usuario, no al procesamiento de archivos de texto. Es lento porque lee stdin char por char, haciendo una llamada al sistema para cada char.
PM 2Ring
@PM 2Ring Mi pregunta no mencionó que la palabra clave está en una línea propia porque no quiero una respuesta que funcione en un número tan limitado de casos: quería algo que pudiera funcionar fácilmente sin importar dónde esté la palabra clave fue. Tampoco dije que mi código es eficiente; si lo fuera, no estaría buscando una alternativa ...
Tal
@Wildcard A menos que me falte algo, printf interpreta absolutamente caracteres especiales, y mucho más que el 'echo' predeterminado. printf "hi\n"hará que printf imprima una nueva línea mientras la echo "hi\n"imprime tal como está.
Tal
@Tal, la "f" printfsignifica "formato": el primer argumento printfes un especificador de formato . Si ese especificador es %s\n, que significa "cadena seguida de nueva línea", nada en el siguiente argumento será interpretado o traducido printf en absoluto . (El shell todavía puede interpretarlo, por supuesto; es mejor pegarlo todo entre comillas simples si es una cadena literal, o comillas dobles si desea una expansión variable). Vea mi respuesta usandoprintf para obtener más detalles.
Comodín el