Repository: JEG2/highline (ruby)

Issue:
Highline v1.6.2 Error
Hi James,

I was working on a chef setup with vagrant and hit a wall with it dying on https://github.com/JEG2/highline/blob/master/lib/highline.rb#L711

```
chef] Destroying VM and associated drives...
[chef] Running cleanup tasks for 'chef_solo' provisioner...
/Users/mdesilva/.vagrant.d/gems/gems/highline-1.6.20/lib/highline.rb:711:in `format_statement': undefined method `to_str' for {"chef-server"=>"2.0.0"}:Hash (NoMethodError)
    from /Users/mdesilva/.vagrant.d/gems/gems/highline-1.6.20/lib/highline.rb:617:in `say'
```

I've 'patched' it locally by hand by changing `to_str` to `to_s`.



Discussion:
If you want to work up a patch for this I'm happy to apply it.

I wouldn't say it's HighLine bug. You're passing Hash to `format_statement` but it doesn't make sense. It's supposed to be a string. Why Hash?

That's a fair point.  I didn't look at that error message closely enough.

It's bubbling up through from Vagrant's use of Berkshelf

```
[chef] Destroying VM and associated drives...
[chef] Running cleanup tasks for 'chef_solo' provisioner...
/Users/mdesilva/.vagrant.d/gems/gems/highline-1.6.20/lib/highline.rb:711:in `format_statement': undefined method `to_str' for {"chef-server"=>"2.0.0"}:Hash (NoMethodError)
    from /Users/mdesilva/.vagrant.d/gems/gems/highline-1.6.20/lib/highline.rb:617:in `say'
    from /Users/mdesilva/.vagrant.d/gems/gems/solve-0.8.1/lib/solve/tracers/human_readable.rb:43:in `solution'
    from /Users/mdesilva/.vagrant.d/gems/gems/solve-0.8.1/lib/solve/solver.rb:124:in `resolve'
    from /Users/mdesilva/.vagrant.d/gems/gems/solve-0.8.1/lib/solve.rb:39:in `it!'
    from /Users/mdesilva/.vagrant.d/gems/gems/berkshelf-2.0.10/lib/berkshelf/resolver.rb:102:in `resolve'
    from /Users/mdesilva/.vagrant.d/gems/gems/berkshelf-2.0.10/lib/berkshelf/berksfile.rb:671:in `resolve'
    from /Users/mdesilva/.vagrant.d/gems/gems/berkshelf-2.0.10/lib/berkshelf/berksfile.rb:418:in `install'`
```

Then we probably need to submit a bug to them, don't you think?

The main output method in Ruby is **#puts**.
**#puts** accepts anything as argument and it just convert it into a String.
So, when we see #say or #ask we naturally think we can pass any arbitrary object, like the hash that was being passed in https://github.com/berkshelf/solve/blob/v0.8.1/lib/solve/tracers/human_readable.rb#L43 and was fixed here https://github.com/berkshelf/solve/commit/6d7540867f5be9585a61dfbe66280bb2b178a082 just to work along with HighLine.
What about to give **#say** some of the  **#puts** superpowers?

The current problem is that **#say** is trying to #dup and #to_str the arguments. But some objects have problems with #dup, and others have problems with #to_str. 

So, I tried to improve on this by first calling String (conversion method), and #dup only after we have a String.


Patch:
diff --git a/lib/highline.rb b/lib/highline.rb
--- a/lib/highline.rb
+++ b/lib/highline.rb
@@ -715,7 +715,7 @@ def output_rows
   private
 
   def format_statement statement
-    statement = (statement || "").dup.to_str
+    statement = String(statement || "").dup
     return statement unless statement.length > 0
 
     template  = ERB.new(statement, nil, "%")


Test patch:
diff --git a/test/tc_highline.rb b/test/tc_highline.rb
--- a/test/tc_highline.rb
+++ b/test/tc_highline.rb
@@ -1094,6 +1094,19 @@ def test_say
     assert_equal("", @output.string)
   end
 
+  def test_say_handles_non_string_argument
+    integer = 10
+    hash    = { :a => 20 }
+
+    assert_nothing_raised { @terminal.say(integer) }
+    assert_equal String(integer), @output.string.chomp
+
+    @output.truncate(@output.rewind)
+
+    assert_nothing_raised { @terminal.say(hash) }
+    assert_equal String(hash), @output.string.chomp
+  end
+
   def test_terminal_size
     assert_instance_of(Fixnum, @terminal.terminal_size[0])
     assert_instance_of(Fixnum, @terminal.terminal_size[1])