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?

Chris Drappier
źródło

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ęć.

Phil Ross
źródło
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 .

chrisk
źródło
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.

einarmagnus
źródło
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!

Phrogz
źródło
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
Brian Campbell
źródło
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
sawa
źródło
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
Rene Saarsoo
źródło
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
Marius Butuc
źródło
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"  
Pyro
źródło
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.

markeissler
źródło
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.

Aivils Štoss
źródło