Can Ruby's YAML module be used to embed comments? - ruby

The to_yaml method produces nice YAML output, but I would like to include comment lines before some of the elements. Is there a way to do so?
For example, I would like to produce:
# hostname or IP address of client
client: host4.example.com
# hostname or IP address of server
server: 192.168.222.222
From something similar to:
{
:client => 'host4.example.com',
:server => '192.168.222.222',
}.to_yaml
... but am not sure if the YAML module even has a way to accomplish.
UPDATE: I ended up not using the solution which used regexes to insert the comments, since it required the separation of the data from the comments. The easiest and most understandable solution for me is:
require 'yaml'
source = <<SOURCE
# hostname or IP address of client
client: host4.example.com
# hostname or IP address of server
server: 192.168.222.222
SOURCE
conf = YAML::load(source)
puts source
The benefit to me is that nothing is repeated (for example, 'client:' is only specified once), the data and comments are together, the source can be outputted as YAML, and the data structure (available in conf) is available for use.

You can do a string replace on all the insertions:
require 'yaml'
source = {
:client => 'host4.example.com',
:server => '192.168.222.222',
}.to_yaml
substitution_list = {
/:client:/ => "# hostname or IP address of client\n:client:",
/:server:/ => "# hostname or IP address of server\n:server:"
}
substitution_list.each do |pattern, replacement|
source.gsub!(pattern, replacement)
end
puts source
output:
---
# hostname or IP address of client
:client: host4.example.com
# hostname or IP address of server
:server: 192.168.222.222

Something like this:
my_hash = {a: 444}
y=YAML::Stream.new()
y.add(my_hash)
y.emit("# this is a comment")
Of course, you will need to walk the input hash yourself and either add() or emit() as needed.
You could look at the source of the to_yaml method for a quick start.

This isn't perfect (no mid-Array support, for example), but it works for my needs.
def commentify_yaml(db)
ret = []
db.to_yaml(line_width: -1).each_line do |l|
if l.match(/^\s*:c\d+:/)
l = l.sub(/:c(\d+)?:/, '#').
sub(/(^\s*# )["']/, '\1').
sub(/["']\s*$/, '').
gsub(/''(\S+?)''/, "'\\1'").
gsub(/(\S)''/, "\\1'")
end
ret << l.chomp
end
ret * "\n"
end
Example usage.
commentify_yaml(
{
c1: 'Comment line 1',
c2: 'Comment line 2',
'hash_1' => {
c1: 'Foo',
c2: 'Bar',
'key_1' => "Hello!",
},
'baz' => qux,
c3: 'Keep up-numbering the comments in the same hash',
'array_1' => [
1,
2,
3
]
}
)
==>
# Comment line 1
# Comment line 2
hash_1:
# Foo
# Bar
key_1: "Hello!"
baz: "Value of qux"
# Keep up-numbering the comments in the same hash
array_1:
- 1
- 2
- 3
(Note: Syck does not indent arrays they way it arguably should.)

Related

Ruby / RSpec - Expect not to include any of

I have the following spec:
describe 'active' do
it 'does not include inactive or deleted records' do
inactive_record= create(:record, :inactive)
deleted_record= create(:record, :deleted)
expect(described_class.active).not_to include inactive_record
expect(described_class.active).not_to include deleted_record
end
end
This is OK when there are two tests, but when I have 10 different statuses I need to check, I'd need to write out ten different expect lines. I can do something like this:
[records_not_to_be_included].each { |record| expect(described_class.active).not_to include record }
But would like to be able to do something like:
expect(described_class.active).not_to include_any_of [records_not_to_be_included]
Is this possible with RSpec?
As you can see, from the docs:
# Passes if actual includes expected. This works for
# collections and Strings. You can also pass in multiple args
# and it will only pass if all args are found in collection.
#
# #example
# expect([1,2,3]).to include(3)
# expect([1,2,3]).to include(2,3)
# expect([1,2,3]).not_to include(4)
# expect("spread").to include("read")
# expect("spread").not_to include("red")
# expect(:a => 1, :b => 2).to include(:b => 2, :a => 1)
# expect(:a => 1, :b => 2).not_to include(:a => 2)
# ...
def include(*expected)
BuiltIn::Include.new(*expected)
end
include accepts one or more elements, so you can try with:
expect(described_class.active).not_to include(inactive_record, deleted_record)

Update hash values with hash key

I'm facing a problem that I couldn't find a working solution yet.
I have my YAML config file for the environment, let's call it development.yml.
This file is used to create the hash that should be updated:
data = YAML.load_file(File.join(Rails.root, 'config', 'environments', 'development.yml'))
What I'm trying to accomplish is something along these lines. Let's suppose we have an element of the sort
data['server']['test']['user']
data['server']['test']['password']
What I want to have is:
data['server']['test']['user'] = #{Server.Test.User}
data['server']['test']['password'] = #{Server.Test.Password}
The idea is to create a placeholder for each value that is the key mapping for that value dynamically, going until the last level of the hash and replacing the value with the mapping to this value, concatenating the keys.
Sorry, it doesn't solve my problem. The location data['server']['test']['user'] will be built dynamically, via a loop that will go through a nested Hash. The only way I found to do it was to append to the string the key for the current iteration of the Hash. At the end, I have a string like "data['server']['test']['name']", which I was thinking on converting to a variable data['server']['test']['name'] and then assigning to this variable the value #{Server.Test.Name}. Reading my question I'm not sure if this is clear, I hope this helps to clarify it.
Input sample:
api: 'active'
server:
test:
user: 'test'
password: 'passwordfortest'
prod:
user: 'nottest'
password: 'morecomplicatedthantest'
In this case, the final result should be to update this yml file in this way:
api: #{Api}
server:
test:
user: #{Server.Test.User}
password: #{Server.Test.Password}
prod:
user: #{Server.Prod.User}
password: #{Server.Prod.Password}
It sounds silly, but I couldn't figure out a way to do it.
I am posting another answer now since I realize what the question is all about.
Use Iteraptor gem:
require 'iteraptor'
require 'yaml'
# or load from file
yaml = <<-YAML.squish
api: 'active'
server:
test:
user: 'test'
password: 'passwordfortest'
prod:
user: 'nottest'
password: 'morecomplicatedthantest'
YAML
mapped =
yaml.iteraptor.map(full_parent: true) do |parent, (k, _)|
v = parent.map(&:capitalize).join('.')
[k, "\#{#{v}}"]
end
puts YAML.dump(mapped)
#⇒ ---
# api: "#{Api}"
# server:
# test:
# user: "#{Server.Test.User}"
# password: "#{Server.Test.Password}"
# prod:
# user: "#{Server.Prod.User}"
# password: "#{Server.Prod.Password}"
puts YAML.dump(mapped).delete('"')
#⇒ ---
# api: #{Api}
# server:
# test:
# user: #{Server.Test.User}
# password: #{Server.Test.Password}
# prod:
# user: #{Server.Prod.User}
# password: #{Server.Prod.Password}
Use String#%:
input = %|
data['server']['host']['name'] = %{server_host}
data['server']['host']['user'] = %{server_host_user}
data['server']['host']['password'] = %{server_host_password}
|
puts (
input % {server_host: "Foo",
server_host_user: "Bar",
server_host_password: "Baz"})
#⇒ data['server']['host']['name'] = Foo
# data['server']['host']['user'] = Bar
# data['server']['host']['password'] = Baz
You can not add key-value pair to a string.
data['server']['host'] # => which results in a string
Option 1:
You can either save Server.Host as host name in the hash
data['server']['host']['name'] = "#{Server.Host}"
data['server']['host']['user'] = "#{Server.Host.User}"
data['server']['host']['password'] = "#{Server.Host.Password}"
Option 2:
You can construct the hash in a single step with Host as key.
data['server']['host'] = { "#{Server.Host}" => {
'user' => "#{Server.Host.User}",
'password' => "#{Server.Host.Password}"
}
}

Ruby retrieve data from file after specific label

How can I get specific data from a file in ruby? I want to get some 10. ip addresses from a file set up like this...
Whatever: xathun
ip_address: 10.2.232.6
etc: aouoeu
more: snthuh
I want to push the ip addresses into an array.
I can pull 10. addresses out of text. I was hoping for a more accurate way to do it as in only the data after the 'ip_address:' label in case there is unwanted matching data.
s_text = File.open("test.txt",'r').read
ip_addresses = s_text.scan(/\d+.\d+.\d+.\d+/)
puts ip_addresses.inspect #=> ["10.2.232.6"]
Here's a simple enough solution.
open('<textfile path>') { |f| puts f.grep(/10\./) }
If the file is setup like that throughout you can do:
arr = []
File.open("text").each_line do |line|
parts = line.split(":")
arr << parts[1].strip if parts[0] == "ip_address"
end
adding to array as you go through once, one line at a time:
ip_data.txt
Whatever: xathun
ip_address: 10.2.232.6
etc: aouoeu
more: snthuh
Whatever: badone
ip_address: 66.8.103.3
etc: huh
more: noooo
Whatever: blah
ip_address: 10.9.244.13
etc: hello
more: goodbye
code
found_tens = []
File.open('ip_data.txt') {|f|
f.each {|line|
line = line.chomp
next if line.empty?
found_tens << $1 if line =~ /^ip_address:\s+(10\.\d+\.\d+\.\d+)/
}
}
p found_tens #["10.2.232.6", "10.9.244.13"]

ruby output format

I have the following code, which works fine. The problem is that I get the following output:
Device ID: SEP1C17D3415659
IP address: 10.2.3.101
I would like to get:
SEP1C17D3415659 10.2.3.101
Thanks for your help
require 'net/telnet'
require '/apps/dateformat'
#tdate = Formatdate.new.mydate
hosts = %w"SW3"
hosts.each do |hostname|
tn = Net::Telnet::new("Host" => "#{hostname}",
"Timeout" => 10000,
"Prompt" => /[$%#>] \z/n)
tn.cmd('String' =>'user' , 'Match'=>/Password:/) { |c| puts c }
tn.cmd('String' =>'password', 'Match'=>/#/) { |c| puts c }
tn.cmd('String' =>'terminal length 0', 'Match'=>/#/) { |c| puts c }
File.open("/agents/#{hostname}-#{#tdate}-agents.txt",'w') do |o|
run=tn.cmd('String' =>'sh cd ne de | inc Device ID: | IP address:', 'Match'=>/#/) { |c| puts c }
run.each_line do |re|
mac = re.match /Device ID: ([\S]+)/
#ip = re.match /([\S]+) address/
ip = re.match /IP address: ([\S]+)/
o.puts mac
o.puts ip
end
end
end
puts automatically prints a new line after each output. Simply put both on the same line.
outputString = mac + " " + ip
o.puts outputString
If you have other output functions available such as print, it will function without putting a newline between the two statements.
You're correctly specify the grouping you want for your regular expression, but you're not using it when printing it out.
Your regular expression is:
mac = re.match /Device ID: ([\S]+)/
and this matches the whole line Device ID: SEP1C17D3415659, putting the part you want (SEP1C17D3415659) into a group so you can get at it later. However you then print it out using
o.puts mac
This gives the whole match, not just the group. What you want is this instead:
o.puts mac[1]
which specifies just the group.
Also, as Cody says, puts adds a newline on each call. If you want both matches to print on the same line try something like:
o.print "#{mac[1]} #{ip[1]}\n"
instead.
Edit:
I'm not familiar with net/telnet or the command you're running, but it looks like you're running a certain command, then filtering for certain lines and then printing some info from those lines. If this is the case, then any lines that don't match will give you nil for mac and ip so you'll get "undefind method []" on those lines.
If this is the case you can simply use:
o.print "#{mac[1]} #{ip[1]}\n" unless mac.nil?
You might find it worth while to restructure your code slightly to better express what you're doing, by combining the two regular expressions into one. Without knowing the structure of the actual lines I can't create the real expression you'll need, but it would look something like this:
run.each_line do |line|
match = line.match /^Device ID: ([\S]+), IP address: ([\S]+)$/ #these are the lines we want
next unless match #skip lines we don't want
o.print "#{match[1]} #{match[2]}\n" #print the details we're interested in
end

Ruby: How can I have a Hash take multiple keys?

I'm taking 5 strings (protocol, source IP and port, destination IP and port) and using them to store some values in a hash. The problem is that if the IPs or ports are switched between source and destination, the key is supposed to be the same.
If I was doing this in C#/Java/whatever I'd have to create a new class and overwrite the hashcode()/equals() methods, but that seems error prone from the little I've read about it and I was wondering if there would be a better alternative here.
I am directly copying a paragraph from Programming Ruby 1.9:
Hash keys must respond to the message hash by returning a hash code, and the hash code for a given key must not change. The keys used in hashes must also be comparable using eql?. If eql? returns true for two keys, then those keys must also have the same hash code. This means that certain classes (such as Array and Hash) can't conveniently be used as keys, because their hash values can change based on their contents.
So you might generate your hash as something like ["#{source_ip} #{source_port}", "#{dest_ip} #{dest_port}", protocol.to_s].sort.join.hash such that the result will be identical when the source and destination are switched.
For example:
source_ip = "1.2.3.4"
source_port = 1234
dest_ip = "5.6.7.8"
dest_port = 5678
protocol = "http"
def make_hash(s_ip, s_port, d_ip, d_port, proto)
["#{s_ip} #{s_port}", "#{d_ip} #{d_port}", proto.to_s].sort.join.hash
end
puts make_hash(source_ip, source_port, dest_ip, dest_port, protocol)
puts make_hash(dest_ip, dest_port, source_ip, source_port, protocol)
This will output the same hash even though the arguments are in a different order between the two calls. Correctly encapsulating this functionality into a class is left as an exercise to the reader.
I think this is what you mean...
irb(main):001:0> traffic = []
=> []
irb(main):002:0> traffic << {:src_ip => "10.0.0.1", :src_port => "9999", :dst_ip => "172.16.1.1", :dst_port => 80, :protocol => "tcp"}
=> [{:protocol=>"tcp", :src_ip=>"10.0.0.1", :src_port=>"9999", :dst_ip=>"172.16.1.1", :dst_port=>80}]
irb(main):003:0> traffic << {:src_ip => "10.0.0.2", :src_port => "9999", :dst_ip => "172.16.1.1", :dst_port => 80, :protocol => "tcp"}
=> [{:protocol=>"tcp", :src_ip=>"10.0.0.1", :src_port=>"9999", :dst_ip=>"172.16.1.1", :dst_port=>80}, {:protocol=>"tcp", :src_ip=>"10.0.0.2", :src_port=>"9999", :dst_ip=>"172.16.1.1", :dst_port=>80}]
The next, somewhat related, question is how to store the IP. You probably want to use the IPAddr object instead of just a string so you can sort the results more easily.
You can use the following code:
def create_hash(prot, s_ip, s_port, d_ip, d_port, value, x = nil)
if x
x[prot] = {s_ip => {s_port => {d_ip => {d_port => value}}}}
else
{prot => {s_ip => {s_port => {d_ip => {d_port => value}}}}}
end
end
# Create a value
h = create_hash('www', '1.2.4.5', '4322', '4.5.6.7', '80', "Some WWW value")
# Add another value
create_hash('https', '1.2.4.5', '4562', '4.5.6.7', '443', "Some HTTPS value", h)
# Retrieve the values
puts h['www']['1.2.4.5']['4322']['4.5.6.7']['80']
puts h['https']['1.2.4.5']['4562']['4.5.6.7']['443']

Resources