Wie entferne ich führende Leerzeichen aus Ruby HEREDOC?

91

Ich habe ein Problem mit einem Ruby-Heredoc, den ich machen möchte. Es gibt das führende Leerzeichen aus jeder Zeile zurück, obwohl ich den Operator - einschließe, der alle führenden Leerzeichen unterdrücken soll. Meine Methode sieht so aus:

    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

und meine Ausgabe sieht so aus:

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

Dies ist natürlich in diesem speziellen Fall richtig, mit Ausnahme aller Leerzeichen zwischen dem ersten "und \ t. Weiß jemand, was ich hier falsch mache?

Chris Drappier
quelle

Antworten:

143

Die <<-Form von Heredoc ignoriert nur führende Leerzeichen für das Endtrennzeichen.

Mit Ruby 2.3 und höher können Sie ein schnörkelloses heredoc ( <<~) verwenden, um das führende Leerzeichen von Inhaltszeilen zu unterdrücken:

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"

Aus der Ruby- Literaldokumentation :

Der Einzug der am wenigsten eingerückten Zeile wird aus jeder Zeile des Inhalts entfernt. Beachten Sie, dass leere Zeilen und Zeilen, die ausschließlich aus wörtlichen Tabulatoren und Leerzeichen bestehen, zum Bestimmen der Einrückung ignoriert werden, maskierte Tabulatoren und Leerzeichen jedoch als Zeichen ohne Einrückung betrachtet werden.

Phil Ross
quelle
11
Ich finde es toll, dass dies auch 5 Jahre nachdem ich die Frage gestellt habe, ein relevantes Thema ist. danke für die aktualisierte antwort!
Chris Drappier
1
@ChrisDrappier Ich bin mir nicht sicher, ob dies möglich ist, aber ich würde vorschlagen, die akzeptierte Antwort auf diese Frage in diese zu ändern, da dies heutzutage eindeutig die Lösung ist.
TheDeadSerious
123

Wenn Sie Rails 3.0 oder höher verwenden, versuchen Sie es #strip_heredoc. In diesem Beispiel aus den Dokumenten werden die ersten drei Zeilen ohne Einrückung gedruckt, wobei die Zwei-Leerzeichen-Einrückung der letzten beiden Zeilen beibehalten wird:

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

In der Dokumentation heißt es außerdem: "Technisch wird nach der am wenigsten eingerückten Zeile in der gesamten Zeichenfolge gesucht und diese Menge an führenden Leerzeichen entfernt."

Hier ist die Implementierung von 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

Die Tests finden Sie in test / core_ext / string_ext_test.rb .

chrisk
quelle
2
Sie können dies auch außerhalb von Rails 3 verwenden!
Bilderstürmer
3
Bilderstürmer ist richtig; nur require "active_support/core_ext/string"zuerst
David J.
2
Scheint in Ruby 1.8.7 nicht zu funktionieren: tryist nicht für String definiert. In der Tat scheint es, dass es ein Schienen-spezifisches Konstrukt ist
Otheus
45

Ich fürchte, ich habe nicht viel zu tun. Normalerweise mache ich:

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

Das funktioniert aber ist ein bisschen ein Hack.

EDIT: Ich lasse mich von Rene Saarsoo inspirieren und schlage stattdessen Folgendes vor:

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

Diese Version sollte funktionieren, wenn die erste Zeile nicht die am weitesten links liegende ist.

einarmagnus
quelle
1
Ich fühle mich schmutzig, weil ich gefragt habe, aber was ist mit dem Hacken des Standardverhaltens von sich EOFselbst und nicht nur String?
Patcon
1
Sicherlich wird das Verhalten von EOF während des Parsens bestimmt. Ich denke, Sie, @patcon, schlagen vor, den Quellcode für Ruby selbst zu ändern, und dann würde sich Ihr Code in anderen Ruby-Versionen anders verhalten.
Einarmagnus
2
Ich wünschte, Rubys Dash-HEREDOC-Syntax würde in Bash eher so funktionieren, dann hätten wir dieses Problem nicht! (Siehe dieses Bash-Beispiel )
TrinitronX
Tipp: \sProbieren Sie eine dieser Zeilen mit Leerzeilen im Inhalt aus und denken Sie daran, dass dies auch Zeilenumbrüche enthält.
Phrogz
Ich habe das auf Ruby 2.2 versucht und kein Problem bemerkt. Was ist mit dir passiert? ( repl.it/B09p )
Einarmagnus
23

Hier ist eine weitaus einfachere Version des nicht eingerückten Skripts, das ich verwende:

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

Verwenden Sie es so:

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

Wenn die erste Zeile möglicherweise stärker als andere eingerückt ist und (wie Rails) basierend auf der am wenigsten eingerückten Zeile nicht eingerückt werden soll, möchten Sie möglicherweise stattdessen Folgendes verwenden:

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

Beachten Sie, dass beim Scannen nach \s+statt [ \t]+möglicherweise Zeilenumbrüche aus Ihrem Heredoc entfernt werden, anstatt Leerzeichen zu führen. Nicht wünschenswert!

Phrogz
quelle
8

<<-In Ruby wird nur das führende Leerzeichen für das Endtrennzeichen ignoriert, sodass es ordnungsgemäß eingerückt werden kann. Das führende Leerzeichen in Zeilen innerhalb der Zeichenfolge wird nicht entfernt, obwohl dies in einigen Online-Dokumentationen möglicherweise steht.

Sie können führende Leerzeichen selbst entfernen, indem Sie Folgendes verwenden 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

Oder wenn Sie nur Leerzeichen entfernen möchten, lassen Sie die Registerkarten:

<<-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
quelle
1
-1 Zum Entfernen aller führenden Leerzeichen anstelle nur des Einrückungsbetrags.
Phrogz
7
@Phrogz Das OP erwähnte, dass er erwartete, dass es "alle führenden Leerzeichen unterdrücken" würde, also gab ich eine Antwort, die dies tat, sowie eine, die nur die Leerzeichen entfernte, nicht die Tabulatoren, falls er danach suchte. Einige Monate später zu kommen, Antworten, die für das OP funktionierten, herunterzustimmen und Ihre eigene konkurrierende Antwort zu veröffentlichen, ist ziemlich lahm.
Brian Campbell
@BrianCampbell Es tut mir leid, dass du so denkst. es war keine Straftat beabsichtigt. Ich hoffe, Sie glauben mir, wenn ich sage, dass ich nicht abstimme, um Stimmen für meine eigene Antwort zu sammeln, sondern einfach, weil ich auf diese Frage durch eine ehrliche Suche nach ähnlichen Funktionen gestoßen bin und die Antworten hier als nicht optimal empfunden habe. Sie haben Recht, dass es den genauen Bedarf des OP löst, aber auch eine etwas allgemeinere Lösung, die mehr Funktionalität bietet. Ich hoffe auch, dass Sie zustimmen, dass Antworten, die nach der Annahme veröffentlicht wurden, für die gesamte Website immer noch wertvoll sind, insbesondere wenn sie Verbesserungen bieten.
Phrogz
4
Schließlich wollte ich den Ausdruck "konkurrierende Antwort" ansprechen. Weder du noch ich sollten im Wettbewerb stehen, noch glaube ich, dass wir es sind. (Wenn ja, gewinnen Sie ab diesem Moment mit 27,4.000 Wiederholungen. :) Wir helfen Personen mit Problemen, sowohl persönlich (OP) als auch anonym (diejenigen, die über Google ankommen). Weitere (gültige) Antworten helfen. In diesem Sinne überdenke ich meine Ablehnung. Sie haben Recht, dass Ihre Antwort nicht schädlich, irreführend oder überbewertet war. Ich habe Ihre Frage jetzt so bearbeitet, dass ich die 2 Wiederholungspunkte vergeben kann, die ich Ihnen weggenommen habe.
Phrogz
1
@Phrogz Tut mir leid, dass ich mürrisch bin; Ich neige dazu, ein Problem mit "-1 für etwas, das ich nicht mag" -Antworten für Antworten zu haben, die das OP angemessen ansprechen. Wenn es bereits hochgestimmte oder akzeptierte Antworten gibt, die fast, aber nicht ganz das tun, was Sie wollen, ist es für jeden in der Zukunft hilfreicher, nur zu klären, wie Sie denken, dass die Antwort in einem Kommentar besser sein könnte, als herunterzustimmen und Veröffentlichen Sie eine separate Antwort, die weit unten angezeigt wird und normalerweise von niemandem gesehen wird, der das Problem hat. Ich stimme nur ab, wenn die Antwort tatsächlich falsch oder irreführend ist.
Brian Campbell
6

Einige andere Antworten finden die Einrückungsebene der geringsten eingekerbten Linie , und löschen Sie, dass aus allen Linien, aber die Art der Vertiefung unter Berücksichtigung bei der Programmierung (dass die erste Zeile ist die am wenigsten eingerückt), ich glaube , Sie für die Vertiefung Niveau aussehen sollte die erste Zeile .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end
sawa
quelle
1
Psst: Was ist, wenn die erste Zeile leer ist?
Phrogz
3

Wie das Originalplakat entdeckte auch ich die <<-HEREDOCSyntax und war verdammt enttäuscht, dass sie sich nicht so verhielt, wie ich es mir vorgestellt hatte.

Aber anstatt meinen Code mit gsub-s zu verunreinigen, habe ich die String-Klasse erweitert:

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
quelle
3
+1 für das Monkeypatch und Entfernen des Leerzeichens beim Einrücken, aber -1 für eine übermäßig komplexe Implementierung.
Phrogz
Stimmen Sie mit Phrogz überein, dies ist wirklich die beste konzeptionelle Antwort, aber die Implementierung ist zu kompliziert
einarmagnus
2

Hinweis: Wie @radiospiel hervorhob, String#squishist es nur im ActiveSupportKontext verfügbar .


Ich glaube Ruby ist String#squish ist näher an dem, wonach Sie wirklich suchen:

So würde ich mit Ihrem Beispiel umgehen:

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
quelle
Vielen Dank für die Ablehnung, aber ich glaube, wir alle sollten besser von einem Kommentar profitieren, der erklärt, warum diese Lösung vermieden werden sollte.
Marius Butuc
1
Nur eine Vermutung, aber String # squish ist wahrscheinlich nicht Teil von Ruby, sondern von Rails. dh es funktioniert nur mit active_support.
Radiospiel
2

Eine andere leicht zu merkende Option ist die Verwendung eines nicht eingerückten Edelsteins

require 'unindent'

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

Ich musste etwas verwenden, mit systemdem ich lange sedBefehle über Zeilen aufteilen und dann Einrückungen UND Zeilenumbrüche entfernen konnte ...

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

Also habe ich mir Folgendes ausgedacht:

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

Standardmäßig werden keine Zeilenumbrüche entfernt, wie bei allen anderen Beispielen.

markeissler
quelle
1

Ich sammle Antworten und habe Folgendes erhalten:

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

Es generiert exzellentes SQL und verlässt nicht die AR-Bereiche.

Aivils Štoss
quelle
Das ist schwer zu lesen.
Sebastian Palma