Come rimuovo i caratteri dello spazio bianco iniziale da Ruby HEREDOC?


91

Ho un problema con un Heredoc Ruby che sto cercando di creare. Restituisce lo spazio bianco iniziale da ogni riga anche se includo l'operatore -, che dovrebbe sopprimere tutti i caratteri dello spazio bianco iniziale. il mio metodo è simile a questo:

    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 il mio output è simile a questo:

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

questo, ovviamente, è giusto in questo caso specifico, tranne per tutti gli spazi tra il primo "e \ t. qualcuno sa cosa sto facendo di sbagliato qui?

Risposte:


143

La <<-forma di heredoc ignora solo gli spazi bianchi iniziali per il delimitatore finale.

Con Ruby 2.3 e versioni successive puoi usare heredoc ( <<~) ondulato per sopprimere lo spazio bianco iniziale delle linee di contenuto:

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"

Dalla documentazione dei letterali Ruby :

Il rientro della riga meno rientrata verrà rimosso da ogni riga del contenuto. Si noti che le righe e le righe vuote costituite esclusivamente da tabulazioni e spazi letterali verranno ignorate ai fini della determinazione del rientro, ma le tabulazioni e gli spazi con escape sono considerati caratteri non di rientro.


11
Mi piace che questo sia ancora un argomento rilevante 5 anni dopo che ho posto la domanda. grazie per la risposta aggiornata!
Chris Drappier

1
@ChrisDrappier Non sono sicuro che sia possibile, ma suggerirei di cambiare la risposta accettata per questa domanda a questa perché oggigiorno questa è chiaramente la soluzione.
TheDeadSerious

123

Se stai usando Rails 3.0 o più recente, prova #strip_heredoc. Questo esempio tratto dalla documentazione stampa le prime tre righe senza indentazione, pur mantenendo l'indentazione di due spazi delle ultime due righe:

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

La documentazione rileva inoltre: "Tecnicamente, cerca la riga meno rientrata nell'intera stringa e rimuove quella quantità di spazi bianchi iniziali".

Ecco l'implementazione da 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 puoi trovare i test in test / core_ext / string_ext_test.rb .


2
Puoi ancora usarlo al di fuori di Rails 3!
iconoclasta

3
iconoclasta è corretto; solo require "active_support/core_ext/string"prima
David J.

2
Non sembra funzionare in ruby ​​1.8.7: trynon è definito per String. In effetti, sembra che si tratti di un costrutto specifico dei binari
Otheus

45

Non c'è molto da fare che io sappia, ho paura. Di solito faccio:

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

Funziona ma è un po 'un trucco.

EDIT: Prendendo ispirazione da Rene Saarsoo di seguito, suggerirei invece qualcosa di simile:

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

Questa versione dovrebbe gestire anche quando la prima riga non è quella più a sinistra.


1
Mi sento sporco per averlo chiesto, ma per quanto riguarda l'hacking del comportamento predefinito di EOFse stesso, piuttosto che solo String?
patcon

1
Sicuramente il comportamento di EOF è determinato durante l'analisi, quindi penso che ciò che tu, @patcon, stai suggerendo implicherebbe la modifica del codice sorgente per Ruby stesso, e quindi il tuo codice si comporterebbe in modo diverso su altre versioni di Ruby.
einarmagnus

2
Vorrei un po 'che la sintassi HEREDOC del trattino di Ruby funzionasse di più in bash, quindi non avremmo questo problema! (Vedi questo esempio di bash )
TrinitronX

Suggerimento: prova uno di questi con righe vuote nel contenuto e poi ricorda che \sinclude le nuove righe.
Phrogz

L'ho provato su Ruby 2.2 e non ho notato alcun problema. Cosa ti è successo? ( repl.it/B09p )
einarmagnus

23

Ecco una versione molto più semplice dello script unindent che 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

Usalo così:

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

Se la prima riga può essere rientrata più di altre e si desidera (come Rails) annullare il rientro in base alla riga meno indentata, si potrebbe invece voler utilizzare:

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

Nota che se cerchi \s+invece di [ \t]+te potresti finire per rimuovere le nuove righe dal tuo heredoc invece di inserire gli spazi bianchi. Non desiderabile!


8

<<-in Ruby ignorerà solo lo spazio iniziale per il delimitatore finale, permettendogli di essere rientrato correttamente. Non elimina lo spazio iniziale sulle righe all'interno della stringa, nonostante ciò che potrebbe dire una certa documentazione in linea.

Puoi rimuovere tu stesso gli spazi bianchi iniziali utilizzando 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 se vuoi semplicemente eliminare gli spazi, lasciando le schede:

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

1
-1 Per eliminare tutti gli spazi bianchi iniziali anziché solo la quantità di rientro.
Phrogz

7
@Phrogz L'OP ha detto che si aspettava che "sopprimesse tutti i caratteri di spazi bianchi iniziali", quindi ho dato una risposta che lo faceva, così come una che rimuoveva solo gli spazi, non le tabulazioni, nel caso fosse quello che stava cercando. In arrivo diversi mesi dopo, il downvoting delle risposte che hanno funzionato per l'OP e la pubblicazione della tua risposta in competizione è un po 'noioso.
Brian Campbell

@BrianCampbell mi dispiace che ti senta in questo modo; nessun reato era inteso. Spero che mi crediate quando dico che non sto votando negativamente nel tentativo di raccogliere voti per la mia risposta, ma semplicemente perché mi sono imbattuto in questa domanda attraverso un'onesta ricerca di funzionalità simili e ho trovato le risposte qui non ottimali. Hai ragione sul fatto che risolve l'esatta esigenza dell'OP, ma lo fa anche una soluzione leggermente più generale che fornisce più funzionalità. Spero anche che tu sia d'accordo sul fatto che le risposte pubblicate dopo che una è stata accettata sono ancora preziose per il sito nel suo insieme, in particolare se offrono miglioramenti.
Phrogz

4
Infine, volevo affrontare la frase "risposta in competizione". Né tu né io dovremmo essere in concorrenza, né credo che lo siamo. (Anche se, se lo siamo, stai vincendo con 27.4k di rep in questo momento. :) Aiutiamo le persone con problemi, sia personalmente (l'OP) che in modo anonimo (coloro che arrivano tramite Google). Più risposte (valide) aiutano. In questo senso, riconsiderare il mio voto negativo. Hai ragione sul fatto che la tua risposta non è stata dannosa, fuorviante o sopravvalutata. Ora ho modificato la tua domanda solo in modo da poterti dare i 2 punti di rep che ti ho tolto.
Phrogz

1
@Phrogz Mi dispiace per essere scontroso; Tendo ad avere un problema con le risposte "-1 per qualcosa che non mi piace" per le risposte che affrontano adeguatamente l'OP. Quando ci sono già risposte votate o accettate che quasi, ma non del tutto, fanno quello che vuoi, tende ad essere più utile per chiunque in futuro chiarire semplicemente come pensi che la risposta potrebbe essere migliore in un commento, piuttosto che votare negativamente e postando una risposta separata che verrà mostrata molto sotto e di solito non verrà vista da nessun altro che ha il problema. Do un voto negativo solo se la risposta è effettivamente sbagliata o fuorviante.
Brian Campbell

6

Alcune altre risposte trovano il livello di indentazione della linea di almeno frastagliata , e cancellare che da tutte le linee, ma considerando la natura del rientro nella programmazione (che la prima linea è il meno rientrato), penso che si dovrebbe cercare il livello di indentazione del la prima riga .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end

1
Psst: cosa succede se la prima riga è vuota?
Phrogz

3

Come il poster originale, anch'io ho scoperto la <<-HEREDOCsintassi e sono rimasto dannatamente deluso dal fatto che non si comportasse come pensavo dovesse comportarsi.

Ma invece di sporcare il mio codice con gsub-s ho esteso la 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

3
+1 per il monkeypatch e rimuovendo solo gli spazi bianchi di rientro, ma -1 per un'implementazione eccessivamente complessa.
Phrogz

D'accordo con Phrogz, questa è davvero la migliore risposta concettualmente, ma l'implementazione è troppo complicata
einarmagnus

2

Nota: come ha sottolineato @radiospiel, String#squishè disponibile solo nel ActiveSupportcontesto.


Credo ruby String#squish è più vicino a ciò che stai veramente cercando:

Ecco come gestirò il tuo esempio:

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

Grazie per il voto negativo, ma credo che tutti noi trarremmo maggior vantaggio da un commento che spiegherebbe perché questa soluzione dovrebbe essere evitata.
Marius Butuc

1
Solo un'ipotesi, ma String # squish probabilmente non fa parte di ruby ​​vero e proprio, ma di Rails; cioè non funzionerà a meno che non si utilizzi active_support.
radiospiel

2

un'altra opzione facile da ricordare è usare gem unindent

require 'unindent'

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

2

Avevo bisogno di usare qualcosa con systemcui potevo dividere i sedcomandi lunghi su più righe e quindi rimuovere il rientro E le nuove righe ...

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

Quindi ho pensato a questo:

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

Il comportamento predefinito è di non rimuovere le nuove righe, proprio come tutti gli altri esempi.


1

Raccolgo risposte e ottengo questo:

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 eccellente SQL e non esce dagli ambiti AR.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.