Làm cách nào để xóa các ký tự khoảng trắng hàng đầu khỏi Ruby HEREDOC?


91

Tôi đang gặp sự cố với một heredoc Ruby mà tôi đang cố tạo ra. Nó trả về khoảng trắng ở đầu mỗi dòng mặc dù tôi đang bao gồm toán tử -, được cho là loại bỏ tất cả các ký tự khoảng trắng ở đầu. phương pháp của tôi trông như thế này:

    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

và đầu ra của tôi trông như thế này:

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

điều này, tất nhiên, là đúng trong trường hợp cụ thể này, ngoại trừ tất cả các khoảng trắng giữa "và \ t. đầu tiên có ai biết tôi đang làm gì sai ở đây không?

Câu trả lời:


143

Các <<-hình thức heredoc chỉ bỏ qua khoảng trắng chủ đạo cho delimiter kết thúc.

Với Ruby 2.3 trở lên, bạn có thể sử dụng heredoc ( <<~) nguệch ngoạc để loại bỏ khoảng trắng đầu dòng nội dung:

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"

Từ tài liệu của Ruby :

Phần thụt lề của dòng ít thụt lề nhất sẽ bị xóa khỏi mỗi dòng của nội dung. Lưu ý rằng các dòng và dòng trống chỉ bao gồm các tab và dấu cách chữ sẽ bị bỏ qua cho mục đích xác định thụt lề, nhưng các tab và dấu cách thoát được coi là các ký tự không thụt lề.


11
Tôi thích rằng đây vẫn là một chủ đề có liên quan 5 năm sau khi tôi đặt câu hỏi. cảm ơn vì phản hồi cập nhật!
Chris Drappier

1
@ChrisDrappier Không chắc liệu điều này có khả thi không, nhưng tôi khuyên bạn nên thay đổi câu trả lời được chấp nhận cho câu hỏi này thành câu trả lời này vì ngày nay đây rõ ràng là giải pháp.
TheDeadSerious

123

Nếu bạn đang sử dụng Rails 3.0 hoặc mới hơn, hãy thử #strip_heredoc. Ví dụ này từ tài liệu in ba dòng đầu tiên không có thụt lề, trong khi vẫn giữ lại thụt lề hai dấu cách của hai dòng cuối cùng:

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

Tài liệu cũng lưu ý: "Về mặt kỹ thuật, nó tìm kiếm dòng thụt lề ít nhất trong toàn bộ chuỗi và loại bỏ lượng khoảng trắng đầu đó."

Đây là cách triển khai từ 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

Và bạn có thể tìm thấy các bài kiểm tra trong test / core_ext / string_ext_test.rb .


2
Bạn vẫn có thể sử dụng nó bên ngoài Rails 3!
iconoclast

3
iconoclast là chính xác; ngay require "active_support/core_ext/string"đầu tiên
David J.

2
Dường như không hoạt động trong ruby ​​1.8.7: trykhông được xác định cho Chuỗi. Trong thực tế, có vẻ như nó là một cấu trúc đường ray cụ thể
Otheus

45

Không có nhiều việc để làm mà tôi biết là tôi sợ. Tôi thường làm:

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

Điều đó hoạt động nhưng là một chút hack.

CHỈNH SỬA: Lấy cảm hứng từ Rene Saarsoo dưới đây, tôi sẽ đề xuất một cái gì đó như thế này thay thế:

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

Phiên bản này sẽ xử lý khi dòng đầu tiên không phải là dòng xa nhất về bên trái.


1
Tôi cảm thấy bẩn khi hỏi, nhưng còn việc hack hành vi mặc định của EOFchính nó, thay vì chỉ String?
patcon 14/09/12

1
Chắc chắn hành vi của EOF được xác định trong quá trình phân tích cú pháp, vì vậy tôi nghĩ những gì bạn, @patcon, đang đề xuất sẽ liên quan đến việc thay đổi mã nguồn cho chính Ruby, và sau đó mã của bạn sẽ hoạt động khác trên các phiên bản Ruby khác.
einarmagnus

2
Tôi ước gì cú pháp HEREDOC gạch ngang của Ruby hoạt động nhiều hơn như vậy trong bash, thì chúng ta sẽ không gặp vấn đề này! (Xem ví dụ bash này )
TrinitronX

Mẹo chuyên nghiệp: hãy thử một trong hai cách này với các dòng trống trong nội dung và sau đó nhớ rằng \sbao gồm các dòng mới.
Phrogz

Tôi đã thử điều đó trên ruby ​​2.2 và không nhận thấy bất kỳ vấn đề nào. Chuyện gì xảy ra với bạn? ( repl.it/B09p )
einarmagnus

23

Đây là một phiên bản đơn giản hơn nhiều của tập lệnh unindent mà tôi sử dụng:

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

Sử dụng nó như vậy:

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

Nếu dòng đầu tiên có thể được thụt lề nhiều hơn những dòng khác và muốn (như Rails) không thụt lề dựa trên dòng ít thụt lề nhất, thay vào đó bạn có thể muốn sử dụng:

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

Lưu ý rằng nếu bạn quét \s+thay vì [ \t]+bạn có thể sẽ loại bỏ các dòng mới khỏi heredoc của bạn thay vì khoảng trắng ở đầu. Không mong muốn!


8

<<-trong Ruby sẽ chỉ bỏ qua khoảng trắng ở đầu cho dấu phân cách kết thúc, cho phép nó được thụt lề đúng cách. Nó không loại bỏ khoảng trống hàng đầu trên các dòng bên trong chuỗi, mặc dù một số tài liệu trực tuyến có thể nói.

Bạn có thể tự mình loại bỏ khoảng trắng ở đầu bằng cách sử dụng 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

Hoặc nếu bạn chỉ muốn tách khoảng trắng, để lại các 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 Để loại bỏ tất cả khoảng trắng ở đầu thay vì chỉ số lượng thụt lề.
Phrogz

7
@Phrogz OP đã đề cập rằng anh ấy mong đợi nó "ngăn chặn tất cả các ký tự khoảng trắng ở đầu", vì vậy tôi đã đưa ra một câu trả lời đã làm được điều đó, cũng như một câu trả lời chỉ xóa khoảng trắng chứ không phải tab, trong trường hợp đó là thứ anh ấy đang tìm kiếm. Đến vài tháng sau, việc phản đối các câu trả lời phù hợp với OP và đăng câu trả lời cạnh tranh của riêng bạn là một điều khập khiễng.
Brian Campbell

@BrianCampbell Tôi xin lỗi vì bạn cảm thấy như vậy; không có ý định xúc phạm. Tôi hy vọng bạn tin tôi khi tôi nói rằng tôi không phản đối trong nỗ lực thu hút phiếu bầu cho câu trả lời của chính mình, mà chỉ đơn giản vì tôi đã đưa ra câu hỏi này thông qua một tìm kiếm trung thực cho chức năng tương tự và nhận thấy câu trả lời ở đây là tối ưu. Bạn nói đúng rằng nó giải quyết được nhu cầu chính xác của OP, nhưng một giải pháp tổng quát hơn một chút cung cấp nhiều chức năng hơn cũng vậy. Tôi cũng hy vọng bạn sẽ đồng ý rằng các câu trả lời được đăng sau khi một câu trả lời đã được chấp nhận vẫn có giá trị đối với trang web nói chung, đặc biệt nếu chúng cung cấp các cải tiến.
Phrogz

4
Cuối cùng, tôi muốn đề cập đến cụm từ "câu trả lời cạnh tranh". Cả bạn và tôi đều không nên cạnh tranh, tôi cũng không tin rằng chúng ta đang có. (Mặc dù nếu đúng như vậy, bạn đang chiến thắng với 27,4 nghìn đại diện tính đến thời điểm này. :) Chúng tôi giúp đỡ những cá nhân gặp vấn đề, cả cá nhân (OP) và ẩn danh (những người đến qua Google). Thêm câu trả lời (hợp lệ) trợ giúp. Trong mạch đó, tôi xem xét lại lời tán thành của mình. Bạn đúng rằng câu trả lời của bạn không có hại, gây hiểu lầm hoặc được đánh giá quá cao. Bây giờ tôi đã chỉnh sửa câu hỏi của bạn chỉ để tôi có thể ban tặng 2 điểm đại diện mà tôi đã lấy đi của bạn.
Phrogz

1
@Phrogz Xin lỗi vì đã gắt gỏng; Tôi có xu hướng gặp vấn đề với câu trả lời "-1 cho điều gì đó tôi không thích" cho các câu trả lời giải quyết đầy đủ OP. Khi đã có những câu trả lời được ủng hộ hoặc được chấp nhận gần như nhưng không hoàn toàn, hãy làm những gì bạn muốn, điều đó có xu hướng hữu ích hơn cho bất kỳ ai trong tương lai nếu chỉ cần làm rõ bạn nghĩ câu trả lời có thể tốt hơn như thế nào trong một nhận xét, thay vì phản đối và đăng một câu trả lời riêng biệt sẽ hiển thị bên dưới và thường sẽ không bị bất kỳ ai khác gặp vấn đề nhìn thấy. Tôi chỉ phản đối nếu câu trả lời thực sự sai hoặc gây hiểu lầm.
Brian Campbell

6

Một số câu trả lời khác tìm ra mức độ thụt đầu dòng của dòng thụt vào ít nhất , và xóa từ tất cả các dòng, nhưng xem xét bản chất của thụt đầu dòng trong lập trình (mà dòng đầu tiên là đơn giản nhất thụt vào), tôi nghĩ bạn nên tìm kiếm mức thụt đầu dòng của các dòng đầu tiên .

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

1
Psst: điều gì sẽ xảy ra nếu dòng đầu tiên trống?
Phrogz

3

Giống như áp phích gốc, tôi cũng đã khám phá ra <<-HEREDOC cú pháp và khá thất vọng vì nó không hoạt động như tôi nghĩ.

Nhưng thay vì rải mã của tôi với gsub-s, tôi đã mở rộng lớp 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 cho Monkeypatch và chỉ loại bỏ khoảng trắng thụt lề, nhưng -1 cho một triển khai quá phức tạp.
Phrogz

Đồng ý với Phrogz, đây thực sự là câu trả lời tốt nhất về mặt khái niệm, nhưng việc thực hiện là quá phức tạp
einarmagnus

2

Lưu ý: Như @radiospiel đã chỉ ra, String#squishchỉ khả dụng trong ActiveSupportngữ cảnh.


tôi tin của ruby String#squish gần hơn với những gì bạn thực sự đang tìm kiếm:

Đây là cách tôi sẽ xử lý ví dụ của bạn:

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

Cảm ơn vì đã bỏ phiếu xuống, nhưng tôi tin rằng tất cả chúng ta sẽ được hưởng lợi tốt hơn từ một nhận xét giải thích lý do tại sao nên tránh giải pháp này.
Marius Butuc

1
Chỉ là phỏng đoán, nhưng Chuỗi # squish có lẽ không phải là một phần của ruby, mà là của Rails; tức là nó sẽ không hoạt động trừ khi sử dụng active_support.
radiospiel,

2

một tùy chọn dễ nhớ khác là sử dụng đá quý unindent

require 'unindent'

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

2

Tôi cần sử dụng một cái gì systemđó mà nhờ đó tôi có thể chia sedcác lệnh dài thành các dòng và sau đó loại bỏ thụt lề VÀ dòng mới ...

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

Vì vậy, tôi đã nghĩ ra điều này:

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

Hành vi mặc định là không tách dòng mới, giống như tất cả các ví dụ khác.


1

Tôi thu thập câu trả lời và nhận được điều này:

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

Nó tạo ra SQL tuyệt vời và không vượt ra khỏi phạm vi AR.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.