Jak usunąć wiodące białe znaki z Ruby HEREDOC?


93

Mam problem z heredocem Ruby, który próbuję zrobić. Zwraca początkowe białe znaki z każdego wiersza, mimo że włączam operator -, który ma blokować wszystkie wiodące białe znaki. moja metoda wygląda następująco:

    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

a moje wyjście wygląda następująco:

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

to oczywiście jest poprawne w tym konkretnym przypadku, z wyjątkiem wszystkich spacji między pierwszym „a \ t. Czy ktoś wie, co tu robię źle?

Odpowiedzi:


145

<<-Forma heredoc ignoruje spacje tylko wiodącą do ogranicznika końcowego.

W Rubim 2.3 i nowszych możesz użyć falistej heredoc ( <<~) do pomijania wiodących białych znaków w wierszach zawartości:

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"

Z dokumentacji literałów Rubiego :

Wcięcie najmniej wciętego wiersza zostanie usunięte z każdego wiersza treści. Zwróć uwagę, że puste wiersze i wiersze składające się wyłącznie z literalnych tabulatorów i spacji zostaną zignorowane przy określaniu wcięć, ale tabulatory ze zmianą znaczenia i spacje są traktowane jako znaki bez wcięć.


12
Podoba mi się, że 5 lat po tym, jak zadałem to pytanie, jest to nadal aktualny temat. dzięki za zaktualizowaną odpowiedź!
— Chris Drappier

1
@ChrisDrappier Nie jestem pewien, czy jest to możliwe, ale proponuję zmienić zaakceptowaną odpowiedź na to pytanie na to, ponieważ obecnie jest to jasne rozwiązanie.
— TheDeadSerious,

123

Jeśli używasz Rails 3.0 lub nowszych, spróbuj #strip_heredoc. Ten przykład z dokumentacji wypisuje pierwsze trzy wiersze bez wcięć, zachowując wcięcie z dwoma spacjami w ostatnich dwóch wierszach:

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

Dokumentacja zauważa również: „Technicznie, szuka najmniej wciętej linii w całym ciągu i usuwa tę ilość wiodących białych spacji”.

Oto implementacja z 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

Testy można znaleźć w test / core_ext / string_ext_test.rb .


2
Nadal możesz używać tego poza Railsami 3!
— ikonoklast

3
ikonoklast ma rację; tylko require "active_support/core_ext/string"pierwszy
— David J.

2
Wygląda na to, że nie działa w Ruby 1.8.7: trynie jest zdefiniowany dla String. W rzeczywistości wydaje się, że jest to konstrukcja specyficzna dla szyn
— Otheus

45

Nie mam wiele do zrobienia, bo boję się. Zwyklę robię:

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

To działa, ale to trochę hack.

EDYCJA: Czerpiąc inspirację z Rene Saarsoo poniżej, sugerowałbym coś takiego:

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

Ta wersja powinna obsługiwać, gdy pierwsza linia nie jest również najbardziej wysunięta na lewo.


1
Czuję się nieprzyjemny, że pytam, ale co z hakowaniem domyślnego zachowania EOFsamego siebie, a nie tylko String?
— patcon

1
Z pewnością zachowanie EOF jest określane podczas parsowania, więc myślę, że to, co sugerujesz @patcon, wymagałoby zmiany kodu źródłowego samego Rubiego, a wtedy twój kod zachowywałby się inaczej na innych wersjach Rubiego.
— einarmagnus

2
Chciałabym, żeby myślnik Ruby składnia HEREDOC działała bardziej tak w bashu, wtedy nie mielibyśmy tego problemu! (Zobacz ten przykład basha )
— TrinitronX

Porada od specjalistów: wypróbuj jedną z nich z pustymi wierszami w treści, a następnie pamiętaj, że \sobejmuje to nowe wiersze.
— Phrogz,

Wypróbowałem to na Ruby 2.2 i nie zauważyłem żadnego problemu. Co się z tobą stało? ( repl.it/B09p )
— einarmagnus

23

Oto znacznie prostsza wersja skryptu bez wcięcia, którego używam:

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

Użyj go w ten sposób:

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

Jeśli pierwsza linia może być wcięta bardziej niż inne i chcesz (tak jak Rails), aby cofnąć wcięcie na podstawie linii z najmniejszym wcięciem, możesz zamiast tego użyć:

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

Zauważ, że jeśli zaczniesz szukać \s+zamiast [ \t]+ciebie, może skończyć się usunięciem nowych linii z twojego heredoc zamiast początkowych białych spacji. Niepożądane!


8

<<-w Rubim zignoruje tylko początkową spację dla separatora końcowego, umożliwiając odpowiednie wcięcie. Nie usuwa wiodących spacji w liniach wewnątrz łańcucha, pomimo tego, co może powiedzieć część dokumentacji online.

Możesz samodzielnie usunąć wiodące białe znaki, używając 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

Lub jeśli chcesz po prostu usunąć spacje, pozostawiając karty:

<<-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 Do usuwania wszystkich wiodących białych znaków zamiast tylko wielkości wcięcia.
— Phrogz,

7
@Phrogz The OP wspomniał, że spodziewał się, że „zatai wszystkie wiodące białe znaki”, więc udzieliłem odpowiedzi, która to zrobiła, a także takiej, która usunęła tylko spacje, a nie tabulatory, na wypadek, gdyby tego właśnie szukał. Pojawienie się kilka miesięcy później, odrzucanie odpowiedzi, które działały dla PO, i publikowanie własnych konkurencyjnych odpowiedzi jest trochę kiepskie.
— Brian Campbell

@BrianCampbell Przykro mi, że tak się czujesz; żadne przestępstwo nie było zamierzone. Mam nadzieję, że mi uwierzysz, kiedy mówię, że nie głosuję w dół, próbując zebrać głosy na własną odpowiedź, ale po prostu dlatego, że natknąłem się na to pytanie, szukając podobnej funkcji i znalazłem tutaj odpowiedzi nieoptymalne. Masz rację, że rozwiązuje on dokładną potrzebę PO, ale tak samo jest z nieco bardziej ogólnym rozwiązaniem, które zapewnia większą funkcjonalność. Mam również nadzieję, że zgodzisz się z tym, że odpowiedzi opublikowane po zaakceptowaniu jednej z nich są nadal wartościowe dla całej witryny, zwłaszcza jeśli oferują ulepszenia.
— Phrogz,

4
Na koniec chciałem odnieść się do wyrażenia „konkurencyjna odpowiedź”. Ani ty, ani ja nie powinniśmy konkurować, ani nie wierzę, że tak jest. (Choć jeśli tak, to wygrywasz z 27,4 tys. Rep.). Pomagamy osobom z problemami, zarówno osobiście (OP), jak i anonimowo (tym, którzy przyjeżdżają przez Google). Więcej (ważnych) odpowiedzi pomaga. W tym duchu ponownie rozważam mój głos przeciw. Masz rację, że Twoja odpowiedź nie była szkodliwa, wprowadzająca w błąd ani przeceniana. Teraz zredagowałem Twoje pytanie, aby móc przyznać Ci 2 punkty reputacji, które odebrałem.
— Phrogz,

1
@Phrogz Przepraszam, że jestem zrzędliwy; Zwykle mam problem z odpowiedziami „-1 za coś, co mi się nie podoba” w przypadku odpowiedzi, które odpowiednio odnoszą się do PO. Kiedy są już przegłosowane lub zaakceptowane odpowiedzi, które prawie, ale nie do końca, robią to, co chcesz, bardziej pomocne będzie dla każdego w przyszłości wyjaśnienie, jak Twoim zdaniem odpowiedź mogłaby być lepsza w komentarzu, a nie w głosowaniu w dół i zamieszczając osobną odpowiedź, która pojawi się znacznie poniżej i zwykle nie będzie widoczna dla nikogo, kto ma problem. Głosuję tylko wtedy, gdy odpowiedź jest rzeczywiście błędna lub myląca.
— Brian Campbell

6

Niektóre inne odpowiedzi określają poziom wcięcia najmniej wciętego wiersza i usuwają go ze wszystkich wierszy, ale biorąc pod uwagę naturę wcięcia w programowaniu (pierwsza linia jest najmniej wcięta), myślę, że powinieneś poszukać poziomu wcięcia w pierwsza linia .

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

1
Psst: co, jeśli pierwsza linia jest pusta?
— Phrogz,

3

Podobnie jak w przypadku oryginalnego plakatu, ja również odkryłem <<-HEREDOCskładnię i byłem cholernie rozczarowany, że nie zachowywał się tak, jak myślałem, że powinien.

Ale zamiast zaśmiecać mój kod gsub-s, rozszerzyłem klasę 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 dla monkeypatch i usuwając tylko białe znaki wcięcia, ale -1 dla zbyt złożonej implementacji.
— Phrogz,

Zgadzam się z Phrogz, to naprawdę najlepsza odpowiedź koncepcyjna, ale implementacja jest zbyt skomplikowana
— einarmagnus

2

Uwaga: jak wskazał @radiospiel, String#squishjest dostępny tylko w ActiveSupportkontekście.


wierzę rubinów String#squish jest bliżej tego, czego naprawdę szukasz:

Oto jak poradziłbym sobie z twoim przykładem:

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

Dziękuję za głosowanie przeciw, ale uważam, że wszyscy skorzystalibyśmy na komentarzu, który wyjaśniałby, dlaczego należy unikać tego rozwiązania.
— Marius Butuc,

1
Zgaduję, ale String # squish prawdopodobnie nie jest częścią właściwego Rubiego, ale Railsów; tzn. nie zadziała, chyba że użyjesz active_support.
— radiospiel

2

Inną łatwą do zapamiętania opcją jest użycie niewciętego klejnotu

require 'unindent'

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

2

Musiałem użyć czegoś, dzięki systemczemu mógłbym podzielić długie sedpolecenia na linie, a następnie usunąć wcięcia ORAZ nowe linie ...

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

Więc wymyśliłem to:

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

Domyślnym zachowaniem jest brak usuwania znaków nowej linii, tak jak we wszystkich innych przykładach.


1

Zbieram odpowiedzi i otrzymuję to:

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

Generuje doskonały SQL i nie wychodzi poza zakres AR.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.