¿Cómo elimino los caracteres de espacios en blanco iniciales de Ruby HEREDOC?

91

Tengo un problema con un heredoc de Ruby que estoy intentando hacer. Devuelve el espacio en blanco inicial de cada línea a pesar de que incluyo el operador -, que se supone que suprime todos los espacios en blanco iniciales. mi método se ve así:

    def distinct_count
    <<-EOF
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

y mi salida se ve así:

    => "            \tSELECT\n            \t CAST('SRC_ACCT_NUM' AS VARCHAR(30)) as
COLUMN_NAME\n            \t,COUNT(DISTINCT SRC_ACCT_NUM) AS DISTINCT_COUNT\n
        \tFROM UD461.MGMT_REPORT_HNB\n"

esto, por supuesto, es correcto en este caso específico, excepto por todos los espacios entre el primer "y \ t. ¿Alguien sabe lo que estoy haciendo mal aquí?

Chris Drappier
fuente

Respuestas:

143

La <<-forma de heredoc solo ignora los espacios en blanco iniciales para el delimitador final.

Con Ruby 2.3 y versiones posteriores, puede usar un heredoc ( <<~) ondulado para suprimir el espacio en blanco inicial de las líneas de contenido:

def test
  <<~END
    First content line.
      Two spaces here.
    No space here.
  END
end

test
# => "First content line.\n  Two spaces here.\nNo space here.\n"

De la documentación de Ruby literals :

La sangría de la línea con menos sangría se eliminará de cada línea del contenido. Tenga en cuenta que las líneas vacías y las líneas que constan únicamente de tabulaciones y espacios literales se ignorarán para determinar la sangría, pero las tabulaciones y los espacios de escape se consideran caracteres sin sangría.

Phil Ross
fuente
11
Me encanta que este siga siendo un tema relevante 5 años después de que hice la pregunta. gracias por la respuesta actualizada!
Chris Drappier
1
@ChrisDrappier No estoy seguro de si esto es posible, pero sugeriría cambiar la respuesta aceptada para esta pregunta a esta, ya que hoy en día esta es claramente la solución.
TheDeadSerious
123

Si está utilizando Rails 3.0 o más reciente, inténtelo #strip_heredoc. Este ejemplo de los documentos imprime las primeras tres líneas sin sangría, mientras que conserva la sangría de dos espacios de las dos últimas líneas:

if options[:usage]
  puts <<-USAGE.strip_heredoc
    This command does such and such.
 
    Supported options are:
      -h         This message
      ...
  USAGE
end

La documentación también señala: "Técnicamente, busca la línea con menos sangría en toda la cadena y elimina esa cantidad de espacios en blanco iniciales".

Aquí está la implementación de active_support / core_ext / string / strip.rb :

class String
  def strip_heredoc
    indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
    gsub(/^[ \t]{#{indent}}/, '')
  end
end

Y puede encontrar las pruebas en test / core_ext / string_ext_test.rb .

chrisk
fuente
2
¡Aún puedes usar esto fuera de Rails 3!
iconoclasta
3
iconoclasta es correcto; solo require "active_support/core_ext/string"primero
David J.
2
No parece funcionar en ruby ​​1.8.7: tryno está definido para String. De hecho, parece que es una construcción específica de rieles
Otheus
45

No hay mucho que hacer que yo sepa, me temo. Yo suelo hacer:

def distinct_count
    <<-EOF.gsub /^\s+/, ""
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

Eso funciona, pero es un truco.

EDITAR: Inspirándome en Rene Saarsoo a continuación, sugeriría algo como esto en su lugar:

class String
  def unindent 
    gsub(/^#{scan(/^\s*/).min_by{|l|l.length}}/, "")
  end
end

def distinct_count
    <<-EOF.unindent
        \tSELECT
        \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
        \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
        \tFROM #{table.call}
    EOF
end

Esta versión debería funcionar cuando la primera línea no es la más a la izquierda.

einarmagnus
fuente
1
Me siento sucio por preguntar, pero ¿qué hay de piratear el comportamiento predeterminado de EOFsí mismo, en lugar de simplemente String?
patcon
1
Seguramente el comportamiento de EOF se determina durante el análisis, así que creo que lo que usted, @patcon, está sugiriendo implicaría cambiar el código fuente de Ruby, y luego su código se comportaría de manera diferente en otras versiones de Ruby.
einarmagnus
2
Me gustaría un poco que la sintaxis HEREDOC del guión de Ruby funcionara más así en bash, ¡entonces no tendríamos este problema! (Vea este ejemplo de bash )
TrinitronX
\sConsejo profesional: pruebe cualquiera de estos con líneas en blanco en el contenido y luego recuerde que incluye nuevas líneas.
Phrogz
Probé eso en ruby ​​2.2 y no noté ningún problema. ¿Qué te pasó? ( repl.it/B09p )
einarmagnus
23

Aquí hay una versión mucho más simple del script sin sangría que uso:

class String
  # Strip leading whitespace from each line that is the same as the 
  # amount of whitespace on the first line of the string.
  # Leaves _additional_ indentation on later lines intact.
  def unindent
    gsub /^#{self[/\A[ \t]*/]}/, ''
  end
end

Úselo así:

foo = {
  bar: <<-ENDBAR.unindent
    My multiline
      and indented
        content here
    Yay!
  ENDBAR
}
#=> {:bar=>"My multiline\n  and indented\n    content here\nYay!"}

Si la primera línea puede tener más sangría que otras y desea (como Rails) eliminar la sangría en función de la línea con menos sangría, puede que desee utilizar:

class String
  # Strip leading whitespace from each line that is the same as the 
  # amount of whitespace on the least-indented line of the string.
  def strip_indent
    if mindent=scan(/^[ \t]+/).min_by(&:length)
      gsub /^#{mindent}/, ''
    end
  end
end

Tenga en cuenta que si busca en \s+lugar de [ \t]+, puede terminar eliminando nuevas líneas de su heredoc en lugar de espacios en blanco iniciales. ¡No deseable!

Phrogz
fuente
8

<<-en Ruby solo ignorará el espacio inicial para el delimitador final, lo que permitirá que tenga una sangría adecuada. No elimina el espacio inicial en las líneas dentro de la cadena, a pesar de lo que pueda decir alguna documentación en línea.

Puede eliminar los espacios en blanco iniciales usted mismo utilizando gsub:

<<-EOF.gsub /^\s*/, ''
    \tSELECT
    \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
    \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
    \tFROM #{table.call}
EOF

O si solo desea eliminar espacios, dejando las pestañas:

<<-EOF.gsub /^ */, ''
    \tSELECT
    \t CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME
    \t,COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
    \tFROM #{table.call}
EOF
Brian Campbell
fuente
1
-1 Para eliminar todos los espacios en blanco iniciales en lugar de solo la cantidad de sangría.
Phrogz
7
@Phrogz El OP mencionó que esperaba que "suprimiera todos los caracteres de espacios en blanco iniciales", así que di una respuesta que hizo eso, así como una que solo quitó los espacios, no las pestañas, en caso de que eso fuera lo que estaba buscando. Llegar varios meses después, rechazar las respuestas que funcionaron para el OP y publicar su propia respuesta competitiva es algo poco convincente.
Brian Campbell
@BrianCampbell Lamento que te sientas así; no se pretendía ofender. Espero que me crean cuando digo que no estoy votando en contra en un intento de obtener votos para mi propia respuesta, sino simplemente porque encontré esta pregunta a través de una búsqueda honesta de una funcionalidad similar y encontré que las respuestas aquí no eran óptimas. Tiene razón en que resuelve la necesidad exacta del OP, pero también lo hace una solución un poco más general que proporciona más funcionalidad. También espero que esté de acuerdo en que las respuestas publicadas después de que una ha sido aceptada siguen siendo valiosas para el sitio en su conjunto, especialmente si ofrecen mejoras.
Phrogz
4
Finalmente, quería abordar la frase "respuesta competitiva". Ni tú ni yo deberíamos competir, ni creo que lo sea. (Aunque si lo estamos, estás ganando con 27.4k de representantes a partir de este momento. :) Ayudamos a las personas con problemas, tanto personalmente (el OP) como de forma anónima (los que llegan a través de Google). Más respuestas (válidas) ayudan. En ese sentido, reconsidero mi voto negativo. Tiene razón en que su respuesta no fue dañina, engañosa o sobrevalorada. Ahora he editado tu pregunta solo para poder otorgar los 2 puntos de reputación que te quité.
Phrogz
1
@Phrogz Perdón por estar gruñón; Tiendo a tener un problema con las respuestas "-1 por algo que no me gusta" para las respuestas que abordan adecuadamente el OP. Cuando ya hay respuestas aprobadas o aceptadas que casi, pero no del todo, hacen lo que quieres, tiende a ser más útil para cualquier persona en el futuro simplemente aclarar cómo crees que la respuesta podría ser mejor en un comentario, en lugar de votar negativamente y publicar una respuesta separada que se mostrará muy por debajo y, por lo general, no será vista por nadie más que tenga el problema. Solo voto en contra si la respuesta es realmente incorrecta o engañosa.
Brian Campbell
6

Algunas otras respuestas encontrar el nivel de sangría de la línea menos sangría , y eliminar que a partir de todas las líneas, pero teniendo en cuenta la naturaleza de la muesca en la programación (que la primera línea es el menos sangría), creo que se debe buscar el nivel de sangría de la primera línea .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end
sawa
fuente
1
Psst: ¿y si la primera línea está en blanco?
Phrogz
3

Al igual que el póster original, yo también descubrí la <<-HEREDOCsintaxis y me decepcionó mucho que no se comportara como yo pensaba que debería comportarse.

Pero en lugar de ensuciar mi código con gsub-s, extendí la clase String:

class String
  # Removes beginning-whitespace from each line of a string.
  # But only as many whitespace as the first line has.
  #
  # Ment to be used with heredoc strings like so:
  #
  # text = <<-EOS.unindent
  #   This line has no indentation
  #     This line has 2 spaces of indentation
  #   This line is also not indented
  # EOS
  #
  def unindent
    lines = []
    each_line {|ln| lines << ln }

    first_line_ws = lines[0].match(/^\s+/)[0]
    re = Regexp.new('^\s{0,' + first_line_ws.length.to_s + '}')

    lines.collect {|line| line.sub(re, "") }.join
  end
end
Rene Saarsoo
fuente
3
+1 para el parche de mono y eliminando solo el espacio en blanco de sangría, pero -1 para una implementación demasiado compleja.
Phrogz
De acuerdo con Phrogz, esta es realmente la mejor respuesta conceptualmente, pero la implementación es demasiado complicada
einarmagnus
2

Nota: Como señaló @radiospiel, String#squishsolo está disponible en el ActiveSupportcontexto.


Yo creo ruby String#squish está más cerca de lo que realmente estás buscando:

Así es como manejaría su ejemplo:

def distinct_count
  <<-SQL.squish
    SELECT
      CAST('#{name}' AS VARCHAR(30)) as COLUMN_NAME,
      COUNT(DISTINCT #{name}) AS DISTINCT_COUNT
      FROM #{table.call}
  SQL
end
Marius Butuc
fuente
Gracias por el voto negativo, pero creo que todos nos beneficiaríamos mejor de un comentario que explicaría por qué debería evitarse esta solución.
Marius Butuc
1
Solo una suposición, pero String # squish probablemente no sea parte de ruby ​​propiamente dicho, sino de Rails; es decir, no funcionará a menos que utilice active_support.
radiospiel
2

otra opción fácil de recordar es usar gema sin sangría

require 'unindent'

p <<-end.unindent
    hello
      world
  end
# => "hello\n  world\n"  
Pyro
fuente
2

Necesitaba usar algo con systemlo que pudiera dividir sedcomandos largos en líneas y luego eliminar la sangría Y las nuevas líneas ...

def update_makefile(build_path, version, sha1)
  system <<-CMD.strip_heredoc(true)
    \\sed -i".bak"
    -e "s/GIT_VERSION[\ ]*:=.*/GIT_VERSION := 20171-2342/g"
    -e "s/GIT_VERSION_SHA1[\ ]:=.*/GIT_VERSION_SHA1 := 2342/g"
    "/tmp/Makefile"
  CMD
end

Entonces se me ocurrió esto:

class ::String
  def strip_heredoc(compress = false)
    stripped = gsub(/^#{scan(/^\s*/).min_by(&:length)}/, "")
    compress ? stripped.gsub(/\n/," ").chop : stripped
  end
end

El comportamiento predeterminado es no eliminar las nuevas líneas, como todos los demás ejemplos.

markeissler
fuente
1

Recopilo respuestas y obtuve esto:

class Match < ActiveRecord::Base
  has_one :invitation
  scope :upcoming, -> do
    joins(:invitation)
    .where(<<-SQL_QUERY.strip_heredoc, Date.current, Date.current).order('invitations.date ASC')
      CASE WHEN invitations.autogenerated_for_round IS NULL THEN invitations.date >= ?
      ELSE (invitations.round_end_time >= ? AND match_plays.winner_id IS NULL) END
    SQL_QUERY
  end
end

Genera un excelente SQL y no se sale de los alcances de AR.

Aivils Štoss
fuente