Como faço para remover os caracteres de espaço em branco iniciais do Ruby HEREDOC?

91

Estou tendo um problema com um heredoc Ruby que estou tentando fazer. Ele está retornando o espaço em branco inicial de cada linha, embora eu esteja incluindo o operador -, que supostamente suprime todos os caracteres de espaço em branco iniciais. meu método se parece com este:

    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

e minha saída fica assim:

    => "            \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"

isso, é claro, está certo neste caso específico, exceto por todos os espaços entre o primeiro "e \ t. alguém sabe o que estou fazendo de errado aqui?

Chris Drappier
fonte

Respostas:

143

A <<-forma de heredoc ignora apenas os espaços em branco iniciais para o delimitador final.

Com Ruby 2.3 e posterior, você pode usar um heredoc ( <<~) squiggly para suprimir o espaço em branco inicial das linhas de conteúdo:

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"

Da documentação dos literais Ruby :

O recuo da linha menos recuada será removido de cada linha do conteúdo. Observe que as linhas vazias e as linhas que consistem apenas em tabulações e espaços literais serão ignoradas para fins de determinação de recuo, mas tabulações e espaços de escape são considerados caracteres de não recuo.

Phil Ross
fonte
11
Adoro que este ainda seja um assunto relevante 5 anos depois de fazer a pergunta. obrigado pela resposta atualizada!
Chris Drappier
1
@ChrisDrappier Não tenho certeza se isso é possível, mas sugiro mudar a resposta aceita para esta pergunta para esta, pois hoje em dia esta claramente é a solução.
TheDeadSerious
123

Se você estiver usando Rails 3.0 ou mais recente, tente #strip_heredoc. Este exemplo da documentação imprime as três primeiras linhas sem recuo, enquanto mantém o recuo de dois espaços das duas últimas linhas:

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

A documentação também observa: "Tecnicamente, ele procura a linha menos recuada em toda a string e remove a quantidade de espaços em branco à esquerda."

Aqui está a implementação 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

E você pode encontrar os testes em test / core_ext / string_ext_test.rb .

chrisk
fonte
2
Você ainda pode usar isso fora do Rails 3!
iconoclasta
3
iconoclasta está correto; apenas require "active_support/core_ext/string"primeiro
David J.
2
Não parece funcionar no ruby ​​1.8.7: trynão está definido para String. Na verdade, parece que é uma construção específica para trilhos
Otheus
45

Não tenho muito a fazer que eu tenha medo. Eu costumo fazer:

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

Isso funciona, mas é um pouco hack.

EDIT: Inspirando-se em Rene Saarsoo abaixo, sugiro algo assim:

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 versão deve ser tratada quando a primeira linha não está mais à esquerda também.

einarmagnus
fonte
1
Eu me sinto sujo por perguntar, mas e quanto a hackear o comportamento padrão em EOFsi, em vez de apenas String?
patcon
1
Certamente o comportamento do EOF é determinado durante a análise, então acho que o que você, @patcon, está sugerindo envolveria a alteração do código-fonte do próprio Ruby, e então seu código se comportaria de maneira diferente em outras versões do Ruby.
einarmagnus de
2
Eu meio que gostaria que a sintaxe do traço HEREDOC do Ruby funcionasse mais assim no bash, então não teríamos esse problema! (Veja este exemplo de bash )
TrinitronX
Dica de profissional: tente qualquer um deles com linhas em branco no conteúdo e lembre-se de que \sinclui novas linhas.
Phrogz
Eu tentei isso no Ruby 2.2 e não notei nenhum problema. O que aconteceu com você? ( repl.it/B09p )
einarmagnus
23

Esta é uma versão muito mais simples do script sem recuo 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

Use-o assim:

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

Se a primeira linha pode ser indentada mais do que outras, e deseja (como Rails) desfazer o indentação com base na linha menos indentada, você pode querer usar:

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

Observe que se você digitalizar em \s+vez de, [ \t]+poderá acabar removendo novas linhas de seu heredoc em vez de espaços em branco à esquerda. Não é desejável!

Phrogz
fonte
8

<<-em Ruby irá apenas ignorar o espaço inicial para o delimitador de finalização, permitindo que ele seja devidamente indentado. Ele não remove o espaço inicial nas linhas dentro da string, apesar do que alguns documentos online podem dizer.

Você mesmo pode remover o espaço em branco inicial usando 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

Ou se você quiser apenas eliminar os espaços, deixando as guias:

<<-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
fonte
1
-1 Para remover todos os espaços em branco iniciais em vez de apenas a quantidade de recuo.
Phrogz
7
@Phrogz O OP mencionou que esperava "suprimir todos os caracteres de espaço em branco iniciais", então dei uma resposta que fez isso, bem como uma que apenas removeu os espaços, não as guias, caso fosse isso que ele estava procurando. Chegar vários meses depois, votar contra as respostas que funcionaram para o OP e postar sua própria resposta concorrente é meio chato.
Brian Campbell
@BrianCampbell Lamento que você se sinta assim; nenhuma ofensa foi intencional. Espero que você acredite em mim quando digo que não estou votando contra ele na tentativa de angariar votos para minha própria resposta, mas simplesmente porque me deparei com essa pergunta por meio de uma busca honesta por funcionalidades semelhantes e achei as respostas aqui abaixo do ideal. Você está certo ao dizer que ele atende às necessidades exatas do OP, mas o mesmo acontece com uma solução um pouco mais geral que oferece mais funcionalidade. Também espero que você concorde que as respostas postadas depois que uma foi aceita ainda são valiosas para o site como um todo, principalmente se oferecerem melhorias.
Phrogz
4
Finalmente, eu queria abordar a frase "resposta competitiva". Nem você nem eu devemos competir, nem acredito que sim. (Embora, se estivermos, você está ganhando com 27,4 mil repetições neste momento. :) Ajudamos indivíduos com problemas, tanto pessoalmente (o OP) quanto anonimamente (aqueles que chegam via Google). Mais respostas (válidas) ajudam. Nesse sentido, reconsidero meu voto negativo. Você está certo ao afirmar que sua resposta não foi prejudicial, enganosa ou superestimada. Acabei de editar sua pergunta apenas para que pudesse conceder os 2 pontos de representação que tirei de você.
Phrogz
1
@Phrogz Desculpe por estar mal-humorado; Costumo ter um problema com respostas "-1 para algo que não gosto" para respostas que abordam adequadamente o OP. Quando já há respostas positivas ou aceitas que quase, mas não exatamente, fazem o que você quer, tende a ser mais útil para qualquer pessoa no futuro apenas esclarecer como você acha que a resposta poderia ser melhor em um comentário, em vez de negar e postando uma resposta separada que aparecerá abaixo e normalmente não será vista por ninguém que tenha o problema. Eu apenas votei negativamente se a resposta for realmente errada ou enganosa.
Brian Campbell,
6

Algumas outras respostas encontrar o nível de recuo da linha menos recuada e excluir que a partir de todas as linhas, mas considerando a natureza de recuo na programação (que a primeira linha é o menos recuado), eu acho que você deve olhar para o nível de recuo do primeira linha .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end
sawa
fonte
1
Psst: e se a primeira linha estiver em branco?
Phrogz
3

Como o pôster original, eu também descobri a <<-HEREDOCsintaxe e fiquei muito desapontado por ela não se comportar como pensei que deveria se comportar.

Mas em vez de encher meu código com gsub-s, eu estendi a classe 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
fonte
3
+1 para o monkeypatch e removendo apenas o espaço em branco de recuo, mas -1 para uma implementação excessivamente complexa.
Phrogz
Concordo com Phrogz, esta realmente é a melhor resposta conceitualmente, mas a implementação é muito complicada
einarmagnus
2

Nota: como @radiospiel apontou, String#squishestá disponível apenas no ActiveSupportcontexto.


Acredito ruby String#squish está mais perto do que você realmente procura:

Aqui está como eu lidaria com seu exemplo:

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
fonte
Obrigado pelo voto negativo, mas acredito que todos nós nos beneficiaríamos melhor com um comentário que explicasse por que essa solução deve ser evitada.
Marius Butuc
1
Só um palpite, mas String # squish provavelmente não faz parte do ruby ​​propriamente dito, mas do Rails; ou seja, não funcionará a menos que use active_support.
radiospiel
2

outra opção fácil de lembrar é usar unindent gem

require 'unindent'

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

Eu precisava usar algo com o systemqual eu pudesse dividir sedcomandos longos entre linhas e, em seguida, remover recuo E novas linhas ...

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

Então eu vim com isso:

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

O comportamento padrão é não remover novas linhas, assim como todos os outros exemplos.

markeissler
fonte
1

Eu coleciono respostas e tenho isto:

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

Gera SQL excelente e não sai dos escopos de AR.

Aivils Štoss
fonte
Isso é difícil de ler.
Sebastian Palma