How to use YAML.load with handlers - ruby

irb(main):001:0> a="run: yes"
=> "run: yes"
irb(main):002:0> require 'yaml'
=> true
irb(main):003:0> YAML.load a
=> {"run"=>true}
irb(main):004:0> YAML.load(a, handlers => {'bool#yes' = identity})
SyntaxError: (irb):4: syntax error, unexpected '=', expecting =>
YAML.load(a, handlers => {'bool#yes' = identity})
^
from /usr/bin/irb:11:in `<main>
I want the yaml val is yes and i google find the handler will help.
But seems i do not use correct syntax.
I try to search related docs but fail.

The problems with the listed code are
that handlers isn't defined anywhere, you likely wanted :handlers
that identity isn't defined anywhere, maybe wanted :identity that
you are missing a > on your hash rocket (=>).
So to get this code to run it should (likely) look like
YAML.load("run: yes", :handlers => {'bool#yes' => :identity})
However, so far as I know the second parameter to YAML.load is a filename.
If you are able to change the input YAML, simply quoting the value "yes" will cause it come through as a string
YAML.load("a: 'yes'")
# => {"a"=>"yes"}
If you require the un-quoted string 'yes' in the YAML to be treated as 'yes', not true in ruby after parsing. I cobbled this together (with help from this question), using Psych::Handler and Pysch::Parser. Though I'm not sure if there's another easier/better way to do this without having to hack this all together like this.
require 'yaml'
class MyHandler < Psych::Handlers::DocumentStream
def scalar(value, anchor, tag, plain, quoted, style)
if value == 'yes'
super(value, anchor, tag, plain, true, style)
else
super(value, anchor, tag, plain, quoted, style)
end
end
end
def my_parse(yaml)
parser = Psych::Parser.new(MyHandler.new{|node| return node})
parser.parse yaml
false
end
my_parse("a: yes").to_ruby
# => {"a"=>"yes"}
my_parse("a: 'yes'").to_ruby
# => {"a"=>"yes"}
my_parse("a: no").to_ruby
# => {"a"=>false}
Sidenote in the console (and the source):
YAML
# => Psych

Related

Why do rspec filter hooks only work with arrow syntax?

In RSpec tests, I'm using hooks/flags to run subsets of tests, similar to what is shown in the examples
# spec_helper.rb
RSpec.configure do |c|
c.filter_run_excluding('broken')
end
This syntax works
# my_spec.rb
describe 'broken test', 'broken' => true do
...
end
This syntax fails with error syntax error, unexpected ':', expecting end-of-input
# my_spec.rb
describe 'broken test', 'broken': true do
...
end
What is the difference between them which causes one to work and the other to fail?
Your first example
{'broken' => true}
# => {"broken" => true}
creates a Hash with a String as the key. When you use the colon syntax however, the hash will have a Symbol key:
{'broken': true} # This is only valid syntax since Ruby 2.2
# => {:broken => true}
{broken: true}
# => {:broken => true}
Since you are specifically excluding specs marked with a String key, the symbol won't match.
You can either change your rspec config to
RSpec.configure do |c|
c.filter_run_excluding(:broken)
end
or continue to use String keys in your specs.
As a tiny post scriptum: the colon-syntax with a quoted string you used in your first spec example is only valid since Ruby 2.2. Older Ruby versions produce the syntax error you quote in your (edited) question.

ruby object to_s gives unexpected output

What is the correct way to view the output of the puts statements below? My apologies for such a simple question.... Im a little rusty on ruby. github repo
require 'active_support'
require 'active_support/core_ext'
require 'indicators'
my_data = Indicators::Data.new(Securities::Stock.new(:symbol => 'AAPL', :start_date => '2012-08-25', :end_date => '2012-08-30').output)
puts my_data.to_s #expected to see Open,High,Low,Close for AAPL
temp=my_data.calc(:type => :sma, :params => 3)
puts temp.to_s #expected to see an RSI value for each data point from the data above
Maybe check out the awesome_print gem.
It provides the .ai method which can be called on anything.
An example:
my_obj = { a: "b" }
my_obj_as_string = my_obj.ai
puts my_obj_as_string
# ... this will print
# {
# :a => "b"
# }
# except the result is colored.
You can shorten all this into a single step with ap(my_obj).
There's also a way to return objects as HTML. It's the my_obj.ai(html: true) option.
Just use .inspect method instead of .to_s if you want to see internal properties of objects.

How do I pass a hash from commandline?

I have a ruby script that has a hash.
Example:
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
I want to add 'snake' => 'hiss'
Example:
myscript.rb --addsound "'snake' => 'hiss'"
Then in my script have it add it to animal_sound.
Example:
animal_sound.merge! 'snake' => 'hiss'
=> {"dog"=>"bark", "cat"=>"meow", "snake"=>"hiss"}
Is there a way to do this?
Here is the whole script:
#!/usr/bin/env ruby
require 'rubygems'
require 'micro-optparse'
options = Parser.new do |p|
p.option :addsound, "add sound"
end.process!
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
if options[:add_sound]
newsound = options[:add_sound]
animal_sound.merge! newsound
end
puts animal_sound
When I run my script I get:
$ bin/myscript.rb --addsound "'snake' => 'hiss'"
bin/myscript.rb:14:in `merge!': can't convert true into Hash (TypeError)
from bin/myscript.rb:14:in `<main>'
SOLVED:
Using PSkocik's solution I got the script to work using animal, sound = options[:addsound].split(' => '); animal_sound[animal] = sound
I also used Simone Carletti's idea to simplify the CLI command. FYI it also works if I want to pass in hash format, like myscript.rb --addsound "'snake' => 'hiss'". Of course the split has to be changed back to split(' => '). I like the simpler CLI using the :.
Example:
myscript.rb --addsound snake:hiss
Final Code:
#!/usr/bin/env ruby
require 'rubygems'
require 'micro-optparse'
options = Parser.new do |p|
p.option :addsound, "add sound", default: ""
end.process!
animal_sound = { 'dog' => 'bark', 'cat' => 'meow' }
if options[:addsound]
animal, sound = options[:addsound].split(':')
animal_sound[animal] = sound
end
puts animal_sound
Command line:
$ bin/myscript.rb --addsound snake:hiss
{"dog"=>"bark", "cat"=>"meow", "snake"=>"hiss"}
I never could get the merge to work.
Each post was helpful. Thanks.
It's a good idea to keep the CLI interface detached from the underlying implementation. In fact, you may decide to switch the script in the future from Ruby to another language, and you don't really want to change the way the code is invoked.
My suggestion is to pass a serialized value, for example
myscript.rb --addsound snake:hiss
In the code, simply decompose the content and merge it.
if options[:add_sound]
animal, sound = options[:add_sound].split(":")
animal_sound.merge!(animal => sound)
end
p.option :addsound, "add sound"
^ this makes it a flag (true or false)
What you want is make it into a switch whose value is the next argument:
p.option :addsound, "add sound", default: ""
^ this makes it a switch, the string value will be assigned to options[:addsound]
newsound = options[:addsound]
^ Here you need to drop the underscore and parse the string into a hash.
Eval is evil.
For example, you could split it on ' => ' and forget about quoting:
newsound = [ options[:addsound].split(' => ') ].to_h #and then merge it
(Passing the argument like so --addsound snake:hiss and then splitting on ':' instead of ' => ' is another good option.)
^splitting on ' => ' should yield a two-member array. Here I put it into another array (arrays of two-member arrays are convertible to hashes) to make it convertible into a hash.
Or you do completely without merging and constructing another hash:
animal, sound = options[:addsound].split(' => ')
animal_sound[animal] = sound
In regards to your error
Notice the line if options[:add_sound]. That basically evaluates to if true. You are getting your error because you are setting newsound to true, and trying to merge a Boolean into a hash. To my knowledge, the .merge only works like so: hash1.merge(hash2).
Passing command line argument
Rather than passing the argument "'snake' => 'hiss'", I suggest making this a comma-delineated list, like so: "snake,hiss". From there, in your if options[:add_sound] block, you can split the string into an array, using a comma as a splitter. Finally, rather than using .merge, you can add your key:value as you normally would for any hash in Ruby. animal_sound[arr[0]] = arr[1].
Mind you, this method will work best with a single key:value pair. I am sure you can submit multiple pairs, but you would need to (by this method) split into more arrays by an additional character(like / maybe).

How do I check if a string is valid YAML?

I'd like to check if a string is valid YAML. I'd like to do this from within my Ruby code with a gem or library. I only have this begin/rescue clause, but it doesn't get rescued properly:
def valid_yaml_string?(config_text)
require 'open-uri'
file = open("https://github.com/TheNotary/the_notarys_linux_mint_postinstall_configuration")
hard_failing_bad_yaml = file.read
config_text = hard_failing_bad_yaml
begin
YAML.load config_text
return true
rescue
return false
end
end
I am unfortunately getting the terrible error of:
irb(main):089:0> valid_yaml_string?("b")
Psych::SyntaxError: (<unknown>): mapping values are not allowed in this context at line 6 column 19
from /home/kentos/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1/psych.rb:203:in `parse'
from /home/kentos/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1/psych.rb:203:in `parse_stream'
from /home/kentos/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1/psych.rb:151:in `parse'
from /home/kentos/.rvm/rubies/ruby-1.9.3-p374/lib/ruby/1.9.1/psych.rb:127:in `load'
from (irb):83:in `valid_yaml_string?'
from (irb):89
from /home/kentos/.rvm/rubies/ruby-1.9.3-p374/bin/irb:12:in `<main>'
Using a cleaned-up version of your code:
require 'yaml'
require 'open-uri'
URL = "https://github.com/TheNotary/the_notarys_linux_mint_postinstall_configuration"
def valid_yaml_string?(yaml)
!!YAML.load(yaml)
rescue Exception => e
STDERR.puts e.message
return false
end
puts valid_yaml_string?(open(URL).read)
I get:
(<unknown>): mapping values are not allowed in this context at line 6 column 19
false
when I run it.
The reason is, the data you are getting from that URL isn't YAML at all, it's HTML:
open('https://github.com/TheNotary/the_notarys_linux_mint_postinstall_configuration').read[0, 100]
=> " \n\n\n<!DOCTYPE html>\n<html>\n <head prefix=\"og: http://ogp.me/ns# fb: http://ogp.me/ns/fb# githubog:"
If you only want a true/false response whether it's parsable YAML, remove this line:
STDERR.puts e.message
Unfortunately, going beyond that and determining if the string is a YAML string gets harder. You can do some sniffing, looking for some hints:
yaml[/^---/m]
will search for the YAML "document" marker, but a YAML file doesn't have to use those, nor do they have to be at the start of the file. We can add that in to tighten up the test:
!!YAML.load(yaml) && !!yaml[/^---/m]
But, even that leaves some holes, so adding in a test to see what the parser returns can help even more. YAML could return an Fixnum, String, an Array or a Hash, but if you already know what to expect, you can check to see what YAML wants to return. For instance:
YAML.load(({}).to_yaml).class
=> Hash
YAML.load(({}).to_yaml).instance_of?(Hash)
=> true
So, you could look for a Hash:
parsed_yaml = YAML.load(yaml)
!!yaml[/^---/m] && parsed_yaml.instance_of(Hash)
Replace Hash with whatever type you think you should get.
There might be even better ways to sniff it out, but those are what I'd try first.

Is it possible to specify formatting options for to_yaml in ruby?

The code
require 'yaml'
puts YAML.load("
is_something:
values: ['yes', 'no']
").to_yaml
produces
---
is_something:
values:
- "yes"
- "no"
While this is a correct yaml, it just looks ugly when you have a hash of arrays. Is there a way for me to get to_yaml to produce the inline array version of the yaml?
An options hash can be passed to to_yaml but how do you use it?
Edit 0: Thanks Pozsár Balázs. But, as of ruby 1.8.7 (2009-04-08 patchlevel 160), the options hash does not work as advertised. :(
irb
irb(main):001:0> require 'yaml'
=> true
irb(main):002:0> puts [[ 'Crispin', 'Glover' ]].to_yaml( :Indent => 4, :UseHeader => true, :UseVersion => true )
---
- - Crispin
- Glover
=> nil
About the hash options: see http://yaml4r.sourceforge.net/doc/page/examples.htm
Ex. 24: Using to_yaml with an options Hash
puts [[ 'Crispin', 'Glover' ]].to_yaml( :Indent => 4, :UseHeader => true, :UseVersion => true )
# prints:
# --- %YAML:1.0
# -
# - Crispin
# - Glover
Ex. 25: Available symbols for an options Hash
Indent: The default indentation to use when emitting (defaults to 2)
Separator: The default separator to use between documents (defaults to '---')
SortKeys: Sort Hash keys when emitting? (defaults to false)
UseHeader: Display the YAML header when emitting? (defaults to false)
UseVersion: Display the YAML version when emitting? (defaults to false)
AnchorFormat: A formatting string for anchor IDs when emitting (defaults to 'id%03d')
ExplicitTypes: Use explicit types when emitting? (defaults to false)
BestWidth: The character width to use when folding text (defaults to 80)
UseFold: Force folding of text when emitting? (defaults to false)
UseBlock: Force all text to be literal when emitting? (defaults to false)
Encoding: Unicode format to encode with (defaults to :Utf8; requires Iconv)
Starting from Ruby 1.9 psych is used as a default YAML engine. It supports some attributes: http://ruby-doc.org/stdlib-2.1.0/libdoc/psych/rdoc/Psych/Handler/DumperOptions.html
So for me it works:
irb(main):001:0> require 'yaml'
=> true
irb(main):002:0> puts [{'a'=> 'b', 'c'=> 'd'}, {'e'=> 'f', 'g'=>'h'}].to_yaml(:indentation => 4)
---
- a: b
c: d
- e: f
g: h
This ugly hack seems to do the trick...
class Array
def to_yaml_style
:inline
end
end
Browsing through ruby's source, I can't find any options I could pass to achieve the same. Default options are described in the lib/yaml/constants.rb.
Just another hack to specify the output style, but this one allows to customize it per specific object, instead of globally (e.g. for all arrays).
https://gist.github.com/jirutka/31b1a61162e41d5064fc
Simple example:
class Movie
attr_accessor :genres, :actors
# method called by psych to render YAML
def encode_with(coder)
# render array inline (flow style)
coder['genres'] = StyledYAML.inline(genres) if genres
# render in default style (block)
coder['actors'] = actors if actors
end
end
The latest versions of Ruby use the Psych module for YAML parsing. There aren't many options that you can pass but you can change indention and line width. Check the latest Psych documentation for more details.
Use Psych directly.
Indentation has no effect:
my_yaml.to_yaml(:indentation => 2)
Indentation works:
Psych.dump(my_yaml, :indentation => 8)

Resources