Як видалити проміжні пробіли з Ruby HEREDOC?


91

У мене проблема з гередоком Рубі, який я намагаюся зробити. Він повертає пробіл з кожного рядка, хоча я включаю оператор -, який повинен придушити всі провідні пробіли. мій метод виглядає так:

    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

і мій результат виглядає так:

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

це, звичайно, правильно в цьому конкретному випадку, за винятком усіх пробілів між першим "і \ t. Хтось знає, що я тут роблю неправильно?

Відповіді:


143

<<-Форма Heredoc тільки ігнорує провідні прогалини для кінцевого обмежувача.

За допомогою Ruby 2.3 і пізніших версій ви можете використовувати кривлячий heredoc ( <<~) для придушення провідного пробілу рядків вмісту:

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"

З документації до літералів Ruby :

Відступ рядка з найменшим відступом буде видалено з кожного рядка вмісту. Зверніть увагу, що порожні рядки та рядки, що складаються виключно з буквальних вкладок та пробілів, будуть ігноруватися для цілей визначення відступу, але екрановані вкладки та пробіли вважаються символами, що не мають відступу.


11
Мені подобається, що це все ще актуальна тема через 5 років після мого запитання. дякую за оновлену відповідь!
Chris Drappier

1
@ChrisDrappier Не впевнений, що це можливо, але я б запропонував змінити прийняту відповідь на це запитання на це, оскільки сьогодні це явно рішення.
TheDeadSerious

123

Якщо ви використовуєте Rails 3.0 або новішу версію, спробуйте #strip_heredoc. Цей приклад із документів друкує перші три рядки без відступу, зберігаючи відступ двох пробілів двох останніх рядків:

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

У документації також зазначається: "Технічно він шукає найменш відступний рядок у всьому рядку та видаляє таку кількість пробілів, що ведуть".

Ось реалізація з 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

І ви можете знайти тести в test / core_ext / string_ext_test.rb .


2
Ви все ще можете використовувати це поза Rails 3!
іконоборці

3
іконоборство правильне; лише require "active_support/core_ext/string"перший
Девід Дж.

2
Здається, не працює в ruby ​​1.8.7: tryне визначено для рядка. Насправді здається, що це конструкція, специфічна для рейок
Отей

45

Не так багато робити, про що я знаю, боюся. Я зазвичай роблю:

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

Це працює, але це трохи хак.

EDIT: Беручи натхнення від Рене Саарсоу нижче, я б запропонував щось на зразок цього:

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

Ця версія повинна оброблятися, коли перший рядок теж не найдальше ліворуч.


1
Я почуваюся брудно за запитання, але як щодо злому самої поведінки за замовчуванням EOF, а не просто String?
patcon

1
Безумовно, поведінка EOF визначається під час синтаксичного аналізу, тому я думаю, що ви, @patcon, припускаєте, передбачає зміну вихідного коду для самого Ruby, і тоді ваш код поводиться по-іншому в інших версіях Ruby.
einarmagnus

2
Мені б хотілося, щоб синтаксис Ruby's тире HEREDOC працював більше так, як у bash, тоді у нас не було б цієї проблеми! (Див. Цей приклад bash )
TrinitronX

Підказка: спробуйте будь-який із них із порожніми рядками у вмісті, а потім пам’ятайте, що це \sвключає нові рядки.
Фрогц

Я спробував це на ruby ​​2.2 і не помітив жодної проблеми. Що з вами сталося? ( repl.it/B09p )
einarmagnus

23

Ось набагато простіша версія сценарію unindent, який я використовую:

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

Використовуйте його так:

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

Якщо перший рядок може бути з відступом більше, ніж інші, і ви хочете (як Rails) зняти відступ на основі найменш відступного рядка, ви можете замість цього використати:

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

Зверніть увагу, що якщо ви скануєте \s+замість цього, [ \t]+ви можете в кінцевому підсумку вилучити нові рядки з вашого heredoc замість пробілів. Не бажано!


8

<<-в Ruby буде ігнорувати лише пробіл для кінцевого роздільника, дозволяючи йому правильно відступити. Він не позбавляє провідного місця на рядках всередині рядка, незважаючи на те, що може сказати деяка документація в Інтернеті.

Ви можете самостійно позбавити пробілів, використовуючи 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

Або якщо ви просто хочете зачистити пробіли, залишивши вкладки:

<<-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 Для видалення всього пробілу, що провідний, замість лише відступу.
Phrogz

7
@Phrogz ОП згадав, що він очікував, що він "придушить усі провідні пробіли", тому я дав відповідь, яка це зробила, а також таку, яка лише позбавила пробіли, а не вкладки, на випадок, якщо це те, що він шукав. Кілька місяців пізніше, голосування проти відповідей, які працювали для ОП, та розміщення власної конкуруючої відповіді є наче кульгавим.
Брайан Кемпбелл,

@BrianCampbell Мені шкода, що ти так почуваєшся; жодне правопорушення не було призначене. Сподіваюсь, ви повірите мені, коли скажу, що я не голосую проти спроби набрати голоси за власну відповідь, а просто тому, що я прийшов до цього питання шляхом чесного пошуку подібних функціональних можливостей і знайшов відповіді тут неоптимальними. Ви маєте рацію, що це вирішує точні потреби операційної системи, але це робить і трохи більш загальне рішення, яке забезпечує більшу функціональність. Я також сподіваюся, що ви погодитесь, що відповіді, опубліковані після того, як один був прийнятий, все ще є цінними для веб-сайту в цілому, особливо якщо вони пропонують вдосконалення.
Phrogz

4
Нарешті, я хотів звернутися до фрази "конкуруюча відповідь". Ні ти, ні я не повинні конкурувати, і я не вірю, що ми є. (Хоча, якщо ми це зробимо, ви виграєте з 27,4 тис. Повторень на цей момент. :) Ми допомагаємо особам, які мають проблеми, як особисто (OP), так і анонімно (тим, хто надходить через Google). Допоможіть більше (дійсних) відповідей. У цьому ключі я переглядаю свій голос проти. Ви праві, що ваша відповідь не була шкідливою, оманливою чи переоціненою. Зараз я відредагував ваше запитання лише для того, щоб я міг назвати 2 пункти представництва, які я забрав у вас.
Phrogz

1
@Phrogz Вибачте за сварку; Я, як правило, маю проблеми з відповідями "-1 за те, що мені не подобається", на відповіді, які адекватно вирішують ОП. Коли вже є прихильні або прийняті відповіді, які майже, але не зовсім, роблять те, що ви хочете, як правило, кориснішим для будь-кого в майбутньому буде просто пояснити, як ви вважаєте, що відповідь може бути кращою в коментарі, а не проти і проти опублікувавши окрему відповідь, яка з’явиться далеко внизу, і зазвичай її не бачитимуть інші, хто має проблему. Я голосую лише проти, якщо відповідь насправді неправильна або вводить в оману.
Брайан Кемпбелл,

6

Деякі інші відповіді знайти рівень відступу найменш відступ лінії , і видалити , що з усіх рядків, але з урахуванням характеру відступу в програмуванні (що перший рядок є найменш відступом), я думаю , ви повинні дивитися на рівень відступу перший рядок .

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

1
Psst: а якщо перший рядок порожній?
Фрогц

3

Як і оригінальний плакат, я теж виявив <<-HEREDOC синтаксис і був шалено розчарований тим, що він поводився не так, як я вважав, що повинен поводитися.

Але замість того, щоб засмічувати свій код gsub-s, я розширив клас 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 для мавпової виправи і зачищення лише пробілів з відступами, але -1 для надто складної реалізації.
Phrogz

Погодьтеся з Phrogz, це дійсно найкраща концептуальна відповідь, але реалізація занадто складна
einarmagnus

2

Примітка: Як зазначив @radiospiel, String#squishдоступний лише в ActiveSupportконтексті.


я вірю рубіновий String#squish ближче до того, що ви насправді шукаєте:

Ось як би я поводився з вашим прикладом:

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

1
Тільки здогадка, але String # squish - це, мабуть, не частина власне рубіну, а Rails; тобто він не буде працювати, якщо не використовувати active_support.
radiospiel

2

ще один легкий для запам’ятовування варіант - використовувати непрозорий самоцвіт

require 'unindent'

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

2

Мені потрібно було використовувати щось, за systemдопомогою чого я міг би розділити довгі sedкоманди по рядках, а потім видалити відступ І нові рядки ...

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

Тож я придумав таке:

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

Поведінка за замовчуванням полягає в тому, щоб не знімати нові рядки, як і всі інші приклади.


1

Я збираю відповіді і отримав таке:

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

Він генерує чудовий SQL і не виходить за рамки AR.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.