Weird backslash substitution en Ruby

No entiendo este código de Ruby:

>> puts '\\ <- single backslash' # \ > puts '\\ <- 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa') # aa <- 2x a, because two backslashes get replaced 

hasta ahora, todo como se esperaba. pero si buscamos 1 con /\\/ y reemplazamos con 2, codificado por '\\\\' , ¿por qué obtenemos esto?

 >> puts '\\ <- only 1 ... replace 1 with 2'.sub(/\\/, '\\\\') # \ <- only 1 backslash, even though we replace 1 with 2 

y luego, cuando codificamos 3 con '\\\\\\' , solo obtenemos 2:

 >> puts '\\ <- only 2 ... 1 with 3'.sub(/\\/, '\\\\\\') # \\ <- 2 backslashes, even though we replace 1 with 3 

¿Alguien puede entender por qué se traga una barra invertida en la cadena de reemplazo? esto sucede en 1.8 y 1.9.

Esto es un problema porque la barra invertida (\) sirve como un carácter de escape para Regexps y Strings. Podría usar la variable especial \ & para reducir el número de barras invertidas en la cadena de reemplazo gsub.

 foo.gsub(/\\/,'\&\&\&') #for some string foo replace each \ with \\\ 

EDITAR: Debo mencionar que el valor de \ & es de una coincidencia Regexp, en este caso una sola barra invertida.

Además, pensé que había una forma especial de crear una cadena que desactivaba el carácter de escape, pero aparentemente no. Ninguno de estos producirá dos barras diagonales:

 puts "\\" puts '\\' puts %q{\\} puts %Q{\\} puts """\\""" puts '''\\''' puts <  

Respuesta rápida

Si desea eludir toda esta confusión, use la syntax de bloque mucho menos confusa . Aquí hay un ejemplo que reemplaza cada barra invertida con 2 barras diagonales inversas:

 "some\\path".gsub('\\') { '\\\\' } 

Detalles horristackntes

El problema es que al usar sub (y gsub ), sin un bloque, ruby ​​interpreta secuencias de caracteres especiales en el parámetro de reemplazo. Desafortunadamente, sub usa la barra diagonal inversa como el carácter de escape para estos:

 \& (the entire regex) \+ (the last group) \` (pre-match string) \' (post-match string) \0 (same as \&) \1 (first captured group) \2 (second captured group) \\ (a backslash) 

Al igual que cualquier escape, esto crea un problema obvio. Si desea incluir el valor literal de una de las secuencias anteriores (p \1 Ej., \1 ) en la cadena de salida, debe escapar de ella. Entonces, para obtener Hello \1 , necesita que la cadena de reemplazo sea Hello \\1 . Y para representar esto como un literal de cadena en Ruby, tienes que escapar de esas barras invertidas de nuevo de esta manera: "Hello \\\\1"

Entonces, hay dos pases de escape diferentes . El primero toma la cadena literal y crea el valor interno de la cadena. El segundo toma ese valor de cadena interno y reemplaza las secuencias anteriores con los datos coincidentes.

Si una barra invertida no es seguida por un carácter que coincida con una de las secuencias anteriores, entonces la barra diagonal inversa (y el carácter que sigue) pasará sin modificaciones. Esto también afecta una barra invertida al final de la cadena, pasará inalterado. Es más fácil ver esta lógica en el código rubinius; solo busque el método to_sub_replacement en la clase String .

Aquí hay algunos ejemplos de cómo String#sub está analizando la cadena de reemplazo:

  • 1 barra invertida \ (que tiene una cadena literal de "\\" )

    Pasa inalterado porque la barra diagonal inversa está al final de la cadena y no tiene caracteres después de ella.

    Resultado: \

  • 2 barras invertidas \\ (que tienen una cadena literal de "\\\\" )

    El par de barras diagonales coinciden con la secuencia de barras invertidas escapada (ver \\ arriba) y se convierte en una sola barra invertida.

    Resultado: \

  • 3 barras diagonales inversas \\\ (que tienen una cadena literal de "\\\\\\" )

    Las dos primeras barras invertidas coinciden con la secuencia \\ y se convierten en una sola barra invertida. Luego, la última barra invertida se encuentra al final de la cadena, por lo que pasa inalterada.

    Resultado: \\

  • 4 barras diagonales inversas \\\\ (que tienen una cadena literal de "\\\\\\\\" )

    Dos pares de barras invertidas coinciden con la secuencia \\ y se convierten en una sola barra invertida.

    Resultado: \\

  • 2 barras invertidas con el carácter en el centro \a\ (que tienen un literal de cadena de "\\a\\" )

    El \a no coincide con ninguna de las secuencias de escape por lo que se permite pasar sin modificaciones. La barra invertida final también está permitida.

    Resultado: \a\

    Nota: El mismo resultado se puede obtener de: \\a\\ (con la cadena literal: "\\\\a\\\\" )

En retrospectiva, esto podría haber sido menos confuso si String#sub hubiera usado un personaje de escape diferente. Entonces no habría necesidad de escapar por partida doble de todas las barras invertidas.

argh, justo después de tipear todo esto, me di cuenta de que \ se usa para referirme a grupos en la cadena de reemplazo. Supongo que esto significa que necesitas un \\ literal en la cadena de reemplazo para obtener uno reemplazado \ . Para obtener un \\ literal, necesita cuatro \ s, de modo que para reemplazar uno con dos, realmente necesita ocho (!).

 # Double every occurrence of \. There's eight backslashes on the right there! >> puts '\\'.sub(/\\/, '\\\\\\\\') 

algo que me estoy perdiendo? ¿formas más eficientes?

Aclarando un poco de confusión en la segunda línea de código del autor.

Tu dijiste:

 >> puts '\\ < - 2x a, because 2 backslashes get replaced'.sub(/\\/, 'aa') # aa <- 2x a, because two backslashes get replaced 

2 barras diagonales inversas no se reemplazan aquí. Está reemplazando 1 barra invertida con dos a's ('aa'). Es decir, si .sub(/\\/, 'a') , solo verías una 'a'

 '\\'.sub(/\\/, 'anything') #=> anything 

el libro de picos menciona este problema exacto, en realidad. aquí hay otra alternativa (de la página 130 de la última edición)

 str = 'a\b\c' # => "a\b\c" str.gsub(/\\/) { '\\\\' } # => "a\\b\\c"