21

How do you have a multiline string with no leading spaces and still be properly aligned with method? Here are some of my attempts. The one that is working is not very fun...

module Something
  def welcome
"
Hello

This is an example.  I have to write this multiline string outside the welcome method
indentation in order for it to be properly formatted on screen. :(
"
  end
end

module Something
  def welcome
    "
    Hello

    This is an example.  I am inside welcome method indentation but for some reason
    I am not working...
    ".ljust(12)
  end
end

module Something
  def welcome
    "Hello\n\n"+
    "This is an example.  I am inside welcome method indentation and properly"+
    "formatted but isn't there a better way?"
  end
end

UPDATE

Here's a method from the ruby style guide:

code = <<-END.gsub(/^\s+\|/, '')
  |def test
  |  some_method
  |  other_method
  |end
END
# => "def test\n  some_method\n  other_method\nend\n"
binarymason
  • 1,351
  • 1
  • 14
  • 31
  • 1
    I think you can't. This http://stackoverflow.com/questions/2337510/ruby-can-i-write-multi-line-string-with-no-concatenation could be helpful – fl00r Nov 04 '15 at 16:44
  • 1
    Does it matter whether it's "fun"? Yes! – Cary Swoveland Nov 04 '15 at 17:07
  • I think the solution from the style guide is good. Though Active Record implements `strip_heredoc`, and that eases the pain, it does little to ease the long term problem of adding new content to the block. Using `|` makes it very easy to see where the margin is. Making it easy to find the margin is a good thing when multiple people work on a project. I'd change the pattern slightly though, to `/^\s*\|/`, to allow it to work even if the block is not indented from the left margin. – the Tin Man Nov 04 '15 at 21:29

4 Answers4

49

As of Ruby 2.3.0, there is a built in method for this: [<<~]

indented = 
<<-EOS
  Hello

  This is an example. I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(
EOS

unindented =
<<~EOS
  Hello

  This is an example. I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(
EOS

puts indented #=>

  Hello

  This is an example. I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(

puts unindented #=>

Hello

This is an example. I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(

Multiline strings in Ruby 2.3 - the squiggly heredoc

Alexander Popov
  • 23,073
  • 19
  • 91
  • 130
5

In the RubyTapas Episode 249, Avdi Grimm describes a technique to strip leading whitespace from a multi-line string:

def unindent(s)
  s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, '')
end

It is behavior compatible to other existing solutions to this problem, e.g. String#strip_heredoc in ActiveSupport / Rails or the standalone unindent gem.

You can use this method with a heredoc which is special syntax in ruby (and many other languages) to write multi-line strings.

module Something
  def unindent(s)
    s.gsub(/^#{s.scan(/^[ \t]+(?=\S)/).min}/, '')
  end

  def welcome
    unindent(<<-TEXT)
      Hello

      This is an example. This multiline string works
        - even with deeper nestings...
      All is OK here :)
    TEXT
  end
end
Holger Just
  • 52,918
  • 14
  • 115
  • 123
  • While I could easily use `active_support/core_ext`, I am going to use this method. I like how it's dry and straight forward and I can have one less dependency. Avdi is the man. Thanks @Holger for bringing this up. – binarymason Nov 04 '15 at 19:41
  • 1
    @m8ss since you mention DRYness, just wanted to point out that it'll actually be DRYer (and a little cleaner) to add the method to the `String` class. That way you can use it anywhere without having to require a module that might not have anything to do with the module you want to use the `unindent` method in. ADDED BONUS: Like `unindent`, it's not an added dependency - simply your own method that you've added to `String`. – jeffdill2 Nov 04 '15 at 20:12
  • awesome. thanks for your help @jeffdill2. I will definitely use this moving forward. I appreciate you volunteering your time for helping the fellow programmer! – binarymason Nov 04 '15 at 20:27
  • Wouldn't tabs throw that off? A tab is ASCII 9, whereas a space is 32, so `[" ", " \t "].min #= "\t "`. (There are meant to be 10 spaces after `" \t"`, but SO shrinks that down.) – Cary Swoveland Nov 04 '15 at 21:01
  • Well, if you mix tabs and spaces in your common prefix so that you have lines with different whitespace prefixes, all bets are off anyway and you deserve the mess you get. So just don't use that and stick to the accepted ruby standard of 2 space indents. And in any case: http://www.emacswiki.org/pics/static/TabsSpacesBoth.png – Holger Just Nov 04 '15 at 22:18
  • I raised that because you have `[ \t]` in your regex. Perhaps you should remove the tab. – Cary Swoveland Nov 05 '15 at 18:23
  • @jeffdill2, many coders rarely, if ever, monkeypatch. There might be trouble in paradise if you created `String#unindent` and a core method of the same name were added to the `String` class in a future version of Ruby. – Cary Swoveland Nov 05 '15 at 18:42
  • @CarySwoveland yep, definitely a possibility. – jeffdill2 Nov 05 '15 at 19:12
  • @CarySwoveland Well, I cited the method as presented by Avdi in his RubyTapas Episode. And I guess having the tab there doesn't isn't a problem. It can only break if you have different prefixes in your heredoc, e.g. if you use tabs in some of them and spaces in others. There is no failsafe way to handle this and results will always be unpredictable. As long as the common prefix in all lines uses the same characters, you can mix tabs and spaces here (although it is still a bad idea to do so) – Holger Just Nov 05 '15 at 22:39
3

You can use a HEREDOC - http://ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html#here_doc - like this:

def welcome
  <<-"welcome".strip_heredoc
    "
    Hello

    This is an example.  I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(
    "
  welcome
end

and use strip_heredoc to remove the indentations - http://apidock.com/rails/String/strip_heredoc.

NOTE:

strip_heredoc is only available if you're using Ruby on Rails (it's a Rails helper). If you're just building this in pure Ruby, unfortunately strip_heredoc won't be available to you. :-(

But fear not pure Ruby users! You can simply lift the source code for strip_heredoc from Rails and add it to Ruby by re-defining the String class. In which case, you can also call the method whatever you want. :-)

Like so:

class String
  def strip_it_real_good
    indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
    gsub(/^[ \t]{#{indent}}/, '')
  end
end

def welcome
  <<-"welcome".strip_it_real_good
    "
    Hello

    This is an example.  I have to write this multiline string outside the welcome method indentation in order for it to be properly formatted on screen. :(
    "
  welcome
end
Community
  • 1
  • 1
jeffdill2
  • 3,968
  • 2
  • 30
  • 47
  • Looks like the question about leading spaces – fl00r Nov 04 '15 at 16:54
  • Whoops! Sorry, I left out an important piece. The answer has been updated. – jeffdill2 Nov 04 '15 at 17:02
  • TIL `strip_heredoc`. ¯\\_(ツ)_/¯ – Cary Swoveland Nov 04 '15 at 17:35
  • @CarySwoveland yeah, `strip_heredoc` is super helpful. – jeffdill2 Nov 04 '15 at 18:24
  • Hi @jeffdill2 thanks! I am working with pure ruby, so that won't work. Helpful and good to know for future reference though. – binarymason Nov 04 '15 at 18:33
  • @m8ss not a problem - you can just add it to Ruby. :-) I've updated my answer. – jeffdill2 Nov 04 '15 at 18:51
  • 2
    @m8ss - `strip_heredoc` is part of Rail's [Active Support core extensions](http://guides.rubyonrails.org/active_support_core_extensions.html#strip-heredoc), so it's easily added to any Ruby script. The core extensions are very useful and should be part of any "plain" Rubyist's toolbox. – the Tin Man Nov 04 '15 at 19:08
  • +1 @theTinMan thanks! simply installing active_support and requiring `active_support/core_ext` worked like a charm. And good to know about Active Support core extensions ... will definitely keep that one under my hat, – binarymason Nov 04 '15 at 19:29
  • 2
    The idea with the core extensions is you don't have to pull in the entire library, which happens if you use `require 'active_support/core_ext'`. Instead, use the explicit path to the gem and cherry-pick only the additions you want: `require 'active_support/core_ext/string/strip'`. That's specified after the documentation for each of the AS methods. – the Tin Man Nov 04 '15 at 19:42
  • Ohhh...(mind blown). I thought the full path was listed at end of documentation in order for you to look up the source code. That is a very cool trick to know. Thanks for the knowledge @theTinMan! – binarymason Nov 04 '15 at 20:24
  • It is also so you can look at it. The Core extensions are nicely written and the way we can grab everything, or related methods, or closely related methods, is great. – the Tin Man Nov 04 '15 at 21:22
-1

I'm not sure I understand the question. Would this (pure Ruby) solution meet your needs?

arr = <<-BITTER_END.split("\n")
  Hello
         Testing, testing.
       I assume the least-indented line is to be left-adjusted and the same amount of leading whitespace is to be removed from all other lines.
BITTER_END
undent = arr.map { |line| line[/^\s*/].size }.min
str = arr.map { |s| s[undent..-1] }.join("\n")

puts str
Hello
     Testing, testing.
   I assume the least-indented line is to be left-adjusted and the same amount of leading whitespace is to be removed from all other lines.
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
  • 1
    This breaks if you have lines with a different amount of leading whitespace. This typically should be preserved to remove only that part of the whitespace which is common to all the lines. – Holger Just Nov 04 '15 at 19:21
  • Thanks, @Holger. As I suspected, I misunderstood the question. I edited my answer. – Cary Swoveland Nov 04 '15 at 20:26