Как мне удалить ведущие пробельные символы из Ruby HEREDOC?

93

У меня проблема с рубиновым heredoc, который я пытаюсь создать. Он возвращает начальные пробелы из каждой строки, даже если я включаю оператор -, который должен подавлять все начальные пробельные символы. мой метод выглядит так:

    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. Кто-нибудь знает, что я здесь делаю неправильно?"

Крис Драппьер
источник

Ответы:

145

<<-Форма 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 :

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

Фил Росс
источник
12
Мне нравится, что это все еще актуальная тема спустя 5 лет после того, как я задал вопрос. спасибо за обновленный ответ!
Крис Драппьер
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!
iconoclast
3
iconoclast правильный; только require "active_support/core_ext/string"первый
Дэвид Дж.
2
Кажется, не работает в ruby ​​1.8.7: tryне определено для String. На самом деле, похоже, что это конструкция, специфичная для рельсов,
Отеус
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

Это работает, но это своего рода взлом.

РЕДАКТИРОВАТЬ: Вдохновленный Рене Саарсу ниже, я бы предложил вместо этого что-то вроде этого:

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

Эта версия должна обрабатывать, когда первая строка тоже не самая крайняя слева.

Einarmagnus
источник
1
Я чувствую себя грязным из-за того, что спрашиваю, но как насчет взлома самого поведения по умолчанию EOF, а не просто String?
patcon
1
Конечно, поведение EOF определяется во время синтаксического анализа, поэтому я думаю, что то, что вы, @patcon, предлагаете, потребует изменения исходного кода для самого Ruby, и тогда ваш код будет вести себя по-другому в других версиях Ruby.
einarmagnus
2
Мне бы хотелось, чтобы синтаксис HEREDOC Ruby dash работал так же в bash, тогда у нас не было бы этой проблемы! (См. Этот пример bash )
TrinitronX
Совет: попробуйте любой из них с пустыми строками в содержимом, а затем помните, что это \sвключает новые строки.
Phrogz
Я попробовал это на Ruby 2.2 и не заметил никаких проблем. Что с тобой случилось? ( repl.it/B09p )
einarmagnus
23

Вот гораздо более простая версия скрипта undent, который я использую:

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 вместо ведущих пробелов. Не желательно!

Phrogz
источник
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 OP упомянул, что он ожидал, что он «подавит все начальные пробельные символы», поэтому я дал ответ, который сделал это, а также тот, который удалял только пробелы, а не вкладки, на случай, если это то, что он искал. Несколько месяцев спустя опускать ответы, которые работали для OP, и публиковать собственный конкурирующий ответ - это своего рода хромота.
Брайан Кэмпбелл,
@BrianCampbell Мне жаль, что ты так думаешь; не было никакого оскорбления. Надеюсь, вы мне поверите, когда я скажу, что я голосую против, пытаясь собрать голоса за свой ответ, а просто потому, что я пришел к этому вопросу в результате честного поиска аналогичных функций и нашел здесь неоптимальные ответы. Вы правы в том, что он решает точную потребность OP, но также делает и несколько более общее решение, обеспечивающее большую функциональность. Я также надеюсь, что вы согласитесь с тем, что ответы, опубликованные после того, как один из них был принят, по-прежнему ценны для сайта в целом, особенно если они предлагают улучшения.
Phrogz
4
Наконец, я хотел обратиться к фразе «конкурирующий ответ». Ни ты, ни я не должны соревноваться, и я не верю в это. (Хотя, если да, то на данный момент вы выигрываете с 27,4 тысячами репутации. :) Мы помогаем людям с проблемами, как лично (OP), так и анонимно (те, кто прибывает через Google). Дополнительные (действительные) ответы помогают. В этом ключе я пересматриваю свой голос против. Вы правы, что ваш ответ не был вредным, вводящим в заблуждение или переоцененным. Я отредактировал ваш вопрос, чтобы я мог наградить 2 очками репутации, которые я забрал у вас.
Phrogz
1
@Phrogz Простите за сварливость; У меня обычно возникает проблема с ответами «-1 за то, что мне не нравится» на ответы, которые адекватно относятся к OP. Когда уже есть одобренные или принятые ответы, которые почти, но не совсем, делают то, что вы хотите, для любого в будущем будет более полезным просто пояснить, как вы думаете, что ответ может быть лучше в комментарии, а не голосование против и отправка отдельного ответа, который появится далеко ниже и обычно не будет виден никому, у кого есть проблема. Я голосую против, только если ответ на самом деле неправильный или вводящий в заблуждение.
Брайан Кэмпбелл,
6

Некоторые другие ответы найти уровень отступа наименее отступ линии , и удалить , что из всех строк, но с учетом характера отступа в программировании (что первая строка является наименее отступом), я думаю , вы должны смотреть на уровень отступа первая линия .

class String
  def unindent; gsub(/^#{match(/^\s+/)}/, "") end
end
сава
источник
1
Psst: что делать, если первая строка пуста?
Phrogz
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
Мариус Бутук
источник
Спасибо за голосование "против", но я считаю, что нам всем будет полезнее комментарий, объясняющий, почему этого решения следует избегать.
Мариус Бутук
1
Просто предположение, но String # squish, вероятно, не является частью собственно ruby, а является частью 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.

Айвилс Штосс
источник