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

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

11 ответов

Решение

<<- форма heredoc игнорирует только начальные пробелы для конечного разделителя.

В Ruby 2.3 и более поздних версиях вы можете использовать волнистый наследственный синдром (<<~) подавить ведущие пробелы в содержательных строках:

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:

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

Если вы используете 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.

Не так много, чтобы делать то, что я знаю, я боюсь. Я обычно делаю:

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

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

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

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

<<- в 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

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

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

Примечание: как отметил @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

Как и оригинальный постер, я тоже обнаружил <<-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

Мне нужно было использовать что-то с 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

Поведение по умолчанию - не убирать символы новой строки, как и во всех других примерах.

Другой легко запоминающийся вариант - использовать неиспользуемый гем

require 'unindent'

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

Я собираю ответы и получаю это:

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.

Другие вопросы по тегам