ฉันจะลบตัวอักษรช่องว่างชั้นนำจาก Ruby HEREDOC ได้อย่างไร


93

ฉันมีปัญหากับ 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 มีใครรู้บ้างว่าฉันทำอะไรผิดที่นี่?

คำตอบ:


145

<<-รูปแบบของ heredoc เพียงละเว้นช่องว่างชั้นนำสำหรับคั่นท้ายที่สุด

ด้วย Ruby 2.3 และใหม่กว่าคุณสามารถใช้ heredoc ( <<~) แบบ squiggly เพื่อยับยั้งช่องว่างชั้นนำของบรรทัดเนื้อหา:

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 :

การเยื้องของบรรทัดที่เยื้องน้อยที่สุดจะถูกลบออกจากแต่ละบรรทัดของเนื้อหา โปรดทราบว่าบรรทัดว่างและบรรทัดที่ประกอบด้วยแท็บและช่องว่างตามตัวอักษรเท่านั้นจะถูกละเว้นเพื่อวัตถุประสงค์ในการกำหนดการเยื้อง แต่แท็บและช่องว่างที่ใช้ Escape จะถือว่าเป็นอักขระที่ไม่มีการเยื้อง


12
ฉันชอบที่นี่ยังคงเป็นหัวข้อที่เกี่ยวข้อง 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

และคุณจะพบการทดสอบในการทดสอบ / core_ext / string_ext_test.rb


2
คุณยังสามารถใช้สิ่งนี้นอก Rails 3 ได้!
iconoclast

3
iconoclast ถูกต้อง แค่require "active_support/core_ext/string"ครั้งแรก
David J.

2
ดูเหมือนจะไม่ทำงานใน Ruby 1.8.7: tryไม่ได้กำหนดไว้สำหรับ String ในความเป็นจริงดูเหมือนว่ามันเป็นโครงสร้างเฉพาะราง
Otheus

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

ใช้งานได้ แต่เป็นบิตของการแฮ็ก

แก้ไข: ได้รับแรงบันดาลใจจาก Rene Saarsoo ด้านล่างฉันขอแนะนำสิ่งนี้แทน:

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
ฉันหวังว่าไวยากรณ์ของรูบี้ HEREDOC จะทำงานได้ดีกว่านี้ในการทุบตีแล้วเราจะไม่มีปัญหานี้! (ดูตัวอย่างการทุบตี )
TrinitronX

เคล็ดลับสำหรับมือโปร: ลองทำอย่างใดอย่างหนึ่งโดยใช้บรรทัดว่างในเนื้อหาจากนั้นจำไว้ว่า\sมีการขึ้นบรรทัดใหม่
Phrogz

ฉันลองใช้กับ 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 OP กล่าวว่าเขาคาดหวังว่ามันจะ "ระงับอักขระช่องว่างที่นำหน้าทั้งหมด" ดังนั้นฉันจึงให้คำตอบว่าทำเช่นนั้นเช่นเดียวกับสิ่งที่ลอกเฉพาะช่องว่างไม่ใช่แท็บในกรณีที่เป็นสิ่งที่เขากำลังมองหา ในอีกหลายเดือนต่อมาการลงคะแนนคำตอบที่ได้ผลสำหรับ OP และการโพสต์คำตอบที่แข่งขันกันของคุณเองนั้นเป็นเรื่องง่อย
Brian Campbell

@BrianCampbell ฉันขอโทษที่คุณรู้สึกแบบนั้น ไม่มีเจตนากระทำผิด ฉันหวังว่าคุณจะเชื่อฉันเมื่อฉันบอกว่าฉันไม่ได้ลงคะแนนในความพยายามที่จะรวบรวมคะแนนสำหรับคำตอบของฉันเอง แต่เพียงเพราะฉันถามคำถามนี้ผ่านการค้นหาฟังก์ชันการทำงานที่คล้ายกันอย่างตรงไปตรงมาและพบคำตอบที่เหมาะสมที่สุด คุณคิดถูกแล้วที่สามารถแก้ปัญหาความต้องการที่แท้จริงของ OP ได้ แต่วิธีแก้ปัญหาทั่วไปเล็กน้อยที่ให้ฟังก์ชันการทำงานมากขึ้น ฉันหวังว่าคุณจะยอมรับว่าคำตอบที่โพสต์หลังจากที่ได้รับการยอมรับแล้วยังคงมีประโยชน์ต่อไซต์โดยรวมโดยเฉพาะอย่างยิ่งหากมีการปรับปรุง
Phrogz

4
สุดท้ายฉันต้องการพูดถึงวลี "คำตอบที่แข่งขันกัน" ทั้งคุณและฉันไม่ควรแข่งขันกันและฉันไม่เชื่อว่าเราเป็นเช่นนั้น (แม้ว่าเราจะเป็นเช่นนั้นคุณก็ชนะด้วยตัวแทน 27.4k ณ ขณะนี้ :)เราช่วยเหลือผู้ที่มีปัญหาทั้งแบบส่วนตัว (OP) และแบบไม่เปิดเผยตัวตน (ผู้ที่มาถึงทาง Google) คำตอบเพิ่มเติม (ถูกต้อง) ช่วยได้ ในหลอดเลือดดำนั้นฉันพิจารณาการลงคะแนนของฉันอีกครั้ง คุณคิดถูกแล้วที่คำตอบของคุณไม่เป็นอันตรายทำให้เข้าใจผิดหรือเกินจริง ตอนนี้ฉันได้แก้ไขคำถามของคุณแล้วเพื่อที่ฉันจะได้มอบ 2 คะแนนของตัวแทนที่ฉันเอาไปจากคุณ
Phrogz

1
@Phrogz ขอโทษที่ไม่พอใจ; ฉันมักจะมีปัญหากับการตอบกลับ "-1 สำหรับสิ่งที่ฉันไม่ชอบ" สำหรับคำตอบที่ตรงกับ OP อย่างเพียงพอ เมื่อมีคำตอบที่ได้รับการโหวตหรือได้รับการยอมรับแล้วซึ่งเกือบจะทำในสิ่งที่คุณต้องการ แต่ไม่มากก็มีแนวโน้มที่จะเป็นประโยชน์มากขึ้นสำหรับทุกคนในอนาคตเพียงแค่ชี้แจงว่าคุณคิดว่าคำตอบจะดีกว่าในความคิดเห็นได้อย่างไรแทนที่จะลงคะแนนและ โพสต์คำตอบแยกต่างหากซึ่งจะแสดงไว้ด้านล่างและโดยปกติจะไม่มีใครเห็นปัญหา ฉันลงคะแนนเฉพาะถ้าคำตอบนั้นผิดหรือทำให้เข้าใจผิด
Brian Campbell

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 สำหรับ Monkeypatch และขีดทับเฉพาะช่องว่างการเยื้อง แต่ -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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.