Bagaimana cara menghapus karakter spasi putih terkemuka dari Ruby HEREDOC?


93

Saya mengalami masalah dengan heredoc Ruby yang saya coba buat. Ini mengembalikan spasi putih utama dari setiap baris meskipun saya termasuk operator -, yang seharusnya menyembunyikan semua karakter spasi putih utama. metode saya terlihat seperti ini:

    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

dan keluaran saya terlihat seperti ini:

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

ini, tentu saja, benar dalam contoh khusus ini, kecuali untuk semua spasi di antara yang pertama "dan \ t. adakah yang tahu apa yang saya lakukan salah di sini?

Jawaban:


145

The <<-bentuk heredoc hanya mengabaikan spasi terkemuka untuk pembatas akhir.

Dengan Ruby 2.3 dan yang lebih baru Anda dapat menggunakan heredoc ( <<~) berlekuk-lekuk untuk menyembunyikan spasi baris konten utama:

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"

Dari dokumentasi literal Ruby :

Indentasi baris dengan indentasi terkecil akan dihapus dari setiap baris konten. Perhatikan bahwa baris dan baris kosong yang hanya terdiri dari tab dan spasi literal akan diabaikan untuk tujuan menentukan indentasi, tetapi tab dan spasi yang lolos dianggap karakter non-indentasi.


12
Saya suka bahwa ini masih menjadi subjek yang relevan 5 tahun setelah saya mengajukan pertanyaan. terima kasih atas tanggapan yang diperbarui!
Chris Drappier

1
@ChrisDrappier Tidak yakin apakah ini mungkin, tapi saya sarankan untuk mengubah jawaban yang diterima untuk pertanyaan ini menjadi yang satu ini karena sekarang ini jelas solusinya.
TheDeadSerious

123

Jika Anda menggunakan Rails 3.0 atau yang lebih baru, coba #strip_heredoc. Contoh dari dokumen ini mencetak tiga baris pertama tanpa lekukan, sambil mempertahankan lekukan dua spasi dari dua baris terakhir:

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

Dokumentasi juga mencatat: "Secara teknis, ini mencari baris yang paling tidak menjorok ke dalam di seluruh string, dan menghapus jumlah spasi di awal."

Berikut implementasi dari 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

Dan Anda dapat menemukan pengujian di test / core_ext / string_ext_test.rb .


2
Anda masih bisa menggunakan ini di luar Rails 3!
iconoclast

3
iconoclast benar; hanya require "active_support/core_ext/string"pertama
David J.

2
Sepertinya tidak berfungsi di ruby ​​1.8.7: trytidak ditentukan untuk String. Faktanya, tampaknya itu adalah konstruksi khusus rel
Otheus

45

Tidak banyak yang harus dilakukan yang saya tahu, saya takut. Saya biasanya melakukan:

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

Itu berfungsi tetapi sedikit hack.

EDIT: Mengambil inspirasi dari Rene Saarsoo di bawah ini, saya menyarankan sesuatu seperti ini sebagai gantinya:

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

Versi ini harus menangani jika baris pertama bukan yang terjauh ke kiri juga.


1
Saya merasa kotor untuk bertanya, tetapi bagaimana dengan meretas perilaku default EOFitu sendiri, bukan hanya String?
patcon

1
Tentunya perilaku EOF ditentukan selama parsing, jadi menurut saya apa yang Anda, @patcon, sarankan akan melibatkan perubahan kode sumber untuk Ruby itu sendiri, dan kemudian kode Anda akan berperilaku berbeda pada versi Ruby lain.
einarmagnus

2
Saya agak berharap sintaks HEREDOC dasbor Ruby bekerja lebih seperti itu di bash, maka kami tidak akan memiliki masalah ini! (Lihat contoh pesta ini )
TrinitronX

Pro-tip: coba salah satu dari ini dengan baris kosong di konten dan kemudian ingat itu \stermasuk baris baru.
Phrogz

Saya mencobanya pada ruby ​​2.2 dan tidak melihat ada masalah. Apa yang terjadi untuk Anda? ( repl.it/B09p )
einarmagnus

23

Berikut adalah versi yang jauh lebih sederhana dari skrip unindent yang saya gunakan:

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

Gunakan seperti ini:

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

Jika baris pertama mungkin lebih menjorok daripada yang lain, dan ingin (seperti Rails) untuk membatalkan indentasi berdasarkan baris yang paling tidak menjorok ke dalam, Anda mungkin ingin menggunakan:

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

Perhatikan bahwa jika Anda memindai, \s+bukan [ \t]+Anda, mungkin akan menghapus baris baru dari heredoc Anda alih-alih memimpin spasi. Tidak diinginkan!


8

<<-di Ruby hanya akan mengabaikan spasi di depan untuk pembatas akhir, memungkinkannya untuk diindentasi dengan benar. Itu tidak menghapus spasi terdepan pada baris di dalam string, terlepas dari apa yang mungkin dikatakan beberapa dokumentasi online.

Anda dapat menghapus spasi di depan sendiri dengan menggunakan 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

Atau jika Anda hanya ingin menghapus spasi, keluar dari tab:

<<-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 Untuk menghapus semua spasi di awal, bukan hanya jumlah indentasi.
Phrogz

7
@Phrogz OP menyebutkan bahwa dia mengharapkannya untuk "menyembunyikan semua karakter spasi putih utama," jadi saya memberikan jawaban yang melakukan itu, serta yang hanya menghapus spasi, bukan tab, jika itu yang dia cari. Datang beberapa bulan kemudian, downvoting jawaban yang berhasil untuk OP, dan posting jawaban bersaing Anda sendiri agak timpang.
Brian Campbell

@BrianCampbell Saya minta maaf Anda merasa seperti itu; tidak ada pelanggaran yang dimaksudkan. Saya harap Anda mempercayai saya ketika saya mengatakan bahwa saya tidak menurunkan suara dalam upaya mengumpulkan suara untuk jawaban saya sendiri, tetapi hanya karena saya menemukan pertanyaan ini melalui pencarian jujur ​​untuk fungsi serupa dan menemukan jawabannya di sini kurang optimal. Anda benar bahwa itu menyelesaikan kebutuhan OP yang tepat, tetapi begitu juga solusi yang sedikit lebih umum yang menyediakan lebih banyak fungsionalitas. Saya juga berharap Anda setuju bahwa jawaban yang diposting setelah diterima masih berharga bagi situs secara keseluruhan, terutama jika mereka menawarkan perbaikan.
Phrogz

4
Akhirnya, saya ingin membahas frase "jawaban bersaing". Baik Anda maupun saya tidak boleh bersaing, saya juga tidak percaya bahwa kita akan bersaing. (Meskipun jika kami menang, Anda menang dengan 27.4k perwakilan pada saat ini. :) Kami membantu individu yang memiliki masalah, baik secara pribadi (OP) dan secara anonim (mereka yang datang melalui Google). Lebih banyak jawaban (valid) membantu. Dalam nada itu, saya mempertimbangkan kembali suara negatif saya. Anda benar bahwa jawaban Anda tidak berbahaya, menyesatkan, atau berlebihan. Saya sekarang telah mengedit pertanyaan Anda hanya agar saya dapat memberikan 2 poin reputasi yang saya ambil dari Anda.
Phrogz

1
@Phrogz Maaf tentang menjadi pemarah; Saya cenderung memiliki masalah dengan jawaban "-1 untuk sesuatu yang tidak saya sukai" untuk jawaban yang cukup membahas OP. Ketika sudah ada jawaban yang disukai atau diterima yang hampir, tetapi tidak cukup, melakukan apa yang Anda inginkan, cenderung lebih membantu bagi siapa pun di masa depan untuk hanya menjelaskan bagaimana menurut Anda jawaban itu bisa lebih baik dalam komentar, daripada merendahkan dan memposting jawaban terpisah yang akan muncul jauh di bawah dan biasanya tidak akan dilihat oleh orang lain yang memiliki masalah. Saya hanya memberikan suara negatif jika jawabannya benar-benar salah atau menyesatkan.
Brian Campbell

6

Beberapa jawaban lain menemukan tingkat lekukan garis paling menjorok , dan menghapus bahwa dari semua lini, tapi mengingat sifat lekukan di pemrograman (yang baris pertama adalah yang paling menjorok), saya pikir Anda harus mencari tingkat lekukan yang baris pertama .

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

1
Ssst: bagaimana jika baris pertama kosong?
Phrogz

3

Seperti poster aslinya, saya juga menemukan <<-HEREDOCsintaksnya dan sangat kecewa karena tidak berperilaku seperti yang saya kira.

Tetapi alih-alih mengotori kode saya dengan gsub-s, saya memperluas kelas 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 untuk monkeypatch dan menghapus hanya spasi indentasi, tetapi -1 untuk implementasi yang terlalu rumit.
Phrogz

Setuju dengan Phrogz, ini benar-benar jawaban terbaik secara konseptual, tetapi penerapannya terlalu rumit
einarmagnus

2

Catatan: Seperti yang ditunjukkan @radiospiel, String#squishhanya tersedia dalam ActiveSupportkonteksnya.


aku percaya ruby String#squish lebih dekat dengan apa yang sebenarnya Anda cari:

Inilah cara saya menangani contoh Anda:

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

Terima kasih atas suara negatifnya, tetapi saya yakin kita semua akan mendapat manfaat yang lebih baik dari komentar yang akan menjelaskan mengapa solusi ini harus dihindari.
Marius Butuc

1
Hanya tebakan, tapi String # squish mungkin bukan bagian dari ruby, tapi Rails; yaitu tidak akan berfungsi kecuali menggunakan active_support.
radiospiel

2

Opsi lain yang mudah diingat adalah menggunakan permata yang tidak memiliki indentasi

require 'unindent'

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

2

Saya perlu menggunakan sesuatu yang systemdengannya saya dapat membagi sedperintah panjang di seluruh baris dan kemudian menghapus indentasi DAN baris baru ...

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

Jadi saya datang dengan ini:

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

Perilaku default adalah tidak menghapus baris baru, seperti semua contoh lainnya.


1

Saya mengumpulkan jawaban dan mendapatkan ini:

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

Ini menghasilkan SQL yang sangat baik dan tidak keluar dari cakupan AR.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.