Conditionally add an extra parameter to an associative hash - ruby

I want to conditionally add an extra parameter to an associative hash.
The existing code looks like this:
:env => {
"ANSIBLE_FORCE_COLOR" => "true",
"ANSIBLE_HOST_KEY_CHECKING" => "#{config.host_key_checking}",
# Ensure Ansible output isn't buffered so that we receive ouput
# on a task-by-task basis.
"PYTHONUNBUFFERED" => 1
},
I want to conditionally add another variable "ANSIBLE_SSH_ARGS" => "-o ForwardAgent=yes" if config.ssh.forward_agent is true.
I could just copy paste, and create an if/else block but surely Ruby has something more elegant?

I solved it like so:
env = {
"ANSIBLE_FORCE_COLOR" => "true",
"ANSIBLE_HOST_KEY_CHECKING" => "#{config.host_key_checking}",
# Ensure Ansible output isn't buffered so that we receive ouput
# on a task-by-task basis.
"PYTHONUNBUFFERED" => 1
}
env["ANSIBLE_SSH_ARGS"] ="-o ForwardAgent=yes" if config.ssh.forward_agent
command << {
:env => env,
:notify => [:stdout, :stderr],
:workdir => #machine.env.root_path.to_s
}
Not sure this is idiomatic Ruby but it worked for me.

Related

Freeze arrays and hashes by default?

Just wondering if something like:
# frozen_string_literal: true
exists but for Array and Hash?
The goal is not having to .freeze every single of those within the same globals file.
I didn't find any library that monkey patches default ruby classes like Array or Hash. But I found an interesting gem immutable-ruby that may fit your needs
Simple example
require "immutable/hash"
person = Immutable::Hash[name: "Simon", gender: :male]
# => Immutable::Hash[:name => "Simon", :gender => :male]
and you cannot just modify values of it, cause it is immutable. You can perform some actions on that hash, but new copy will be returned to you
friend = person.put(:name, "James") # => Immutable::Hash[:name => "James", :gender => :male]
person # => Immutable::Hash[:name => "Simon", :gender => :male]
friend[:name] # => "James"
person[:name] # => "Simon"
Found a way to handle it without using another gem using only vscode and rubocop :
Install the rubocop extension on vscode
Open your .vscode/settings.json
Append those rules :
{
"editor.formatOnSave": true,
"editor.formatOnSaveTimeout": 5000,
"ruby.format": "rubocop"
}
save
enjoy
Thanks to Tom Lord for the hint.

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).

Chef template loop: can't convert Chef::Node::immutableMash into String

I've got a Vagrant setup in which I'm trying to use Chef-solo to generate an conf file which loops though defined variables to pass to the application. Everything is working except the loop and I'm not familiar enough with Ruby/Chef to spot the error.
I'm going to lay out the whole chain of events in case there is something along the way that is the problem, but the first portions of this process seem to work fine.
A config file is written in yaml and includes env variable definitions to be passed:
...
variables:
- DEBUG: 2
...
The config file is read in by the Vagrantfile into a ruby hash and used to create the Chef json nodes:
...
settings = YAML::load(File.read("config.yaml"))
# Provision The Virtual Machine Using Chef
config.vm.provision "chef_solo" do |chef|
chef.json = {
"mysql" => {"server_root_password" => "secret"},
"postgresql" => {"password" => {"postgres" => "secret"}},
"nginx" => {"pid" => "/run/nginx.pid"},
"php-fpm" => {"pid" => "/run/php5-fpm.pid"},
"databases" => settings["databases"] || [],
"sites" => settings["sites"] || [],
"variables" => settings["variables"] || []
}
...
A bunch of chef cookbooks are run (apt, php, nginx, mysql etc) and finally my custom cookbook which is whats giving me grief. The portion of the cookbook responsible for creating a the conf file is shown here:
# Configure All Of The Server Environment Variables
template "#{node['php-fpm']['pool_conf_dir']}/vars.conf" do
source "vars.erb"
owner "root"
group "root"
mode 0644
variables(
:vars => node['variables']
)
notifies :restart, "service[php-fpm]"
end
And the vars.erb is just a one-liner
<%= #vars.each {|key, value| puts "env[" + key + " = " + value } %>
So, when I run all this chef spits out an error about not being able to convert a hash to a string.
can't convert Chef::Node::immutableMash into String
So for some reason this is coming across as an immutableMash and the value of key ends up being the hash [{"DEBUG"=>2}] and value ends up a nil object, but I'm not sure why or how to correct it.
The hash is ending up as the value of key in your example because the YAML file declares DEBUG: 2 as a list member of variables. This translates to variables being an array with a single hash member.
Try changing the template code to this:
<%= #vars[0].each {|key, value| puts "env[" + key + " = " + value } %>
Or try changing the YAML to this and not changing the template code:
variables:
DEBUG: 2
Either change will get your template loop iterating over the hash that you are expecting.

How to add to the extensions for an existing type in Ruby's MIME::Types

MIME::Types recognises txt as text/plain
require 'mime/types'
MIME::Types.type_for("txt").first.to_s # => "text/plain"
I want it to do the same thing for tab, which it doesn't by default
MIME::Types.type_for("tab").first.to_s # => ""
So given that:
MIME::Types['text/plain'].first.extensions
is ["txt", "asc", "c", "cc", "h", "hh", "cpp", "hpp", "dat", "hlp"], why doesn't the following work:
MIME::Types['text/plain'].first.extensions.push("tab")
MIME::Types.type_for("tab").first.to_s # => still just ""
Mime::Type doesn't appear to have any methods for adding extensions to an existing registered handler. What you can do is convert an existing handler to a hash, add in your own extension, then re-register the handler. This will output a warning, but it will work:
text_plain = MIME::Types['text/plain'].first.to_hash
text_plain['Extensions'].push('tab')
MIME::Types.add(MIME::Type.from_hash(text_plain))
MIME::Types.type_for("tab").first.to_s # => 'text/plain'
Or if you want to be clever and confusing and do it all in one line:
MIME::Types.add(MIME::Type.from_hash(MIME::Types['text/plain'].first.to_hash.tap{ |text_plain| text_plain['Extensions'].push('tab') }))
MIME::Types.type_for("tab").first.to_s # => 'text/plain'
If for some reason you need to suppress the warning message, you can do it like this (assuming you are running the code on a linux-y system):
orig_stdout = $stdout
$stdout = File.new('/dev/null', 'w')
# insert the code block from above
$stdout = orig_stdout
another way is to creating a new content type, e.g
stl_mime_type_hash = MIME::Type.new('application/vnd.ms-pkistl').to_hash
stl_mime_type_hash['Extensions'].push('stl')
MIME::Types.add(MIME::Type.from_hash(stl_mime_type_hash))

How to check if a directory/file/symlink exists with one command in Ruby

Is there a single way of detecting if a directory/file/symlink/etc. entity (more generalized) exists?
I need a single function because I need to check an array of paths that could be directories, files or symlinks. I know File.exists?"file_path" works for directories and files but not for symlinks (which is File.symlink?"symlink_path").
The standard File module has the usual file tests available:
RUBY_VERSION # => "1.9.2"
bashrc = ENV['HOME'] + '/.bashrc'
File.exist?(bashrc) # => true
File.file?(bashrc) # => true
File.directory?(bashrc) # => false
You should be able to find what you want there.
OP: "Thanks but I need all three true or false"
Obviously not. Ok, try something like:
def file_dir_or_symlink_exists?(path_to_file)
File.exist?(path_to_file) || File.symlink?(path_to_file)
end
file_dir_or_symlink_exists?(bashrc) # => true
file_dir_or_symlink_exists?('/Users') # => true
file_dir_or_symlink_exists?('/usr/bin/ruby') # => true
file_dir_or_symlink_exists?('some/bogus/path/to/a/black/hole') # => false
Why not define your own function File.exists?(path) or File.symlink?(path) and use that?
Just File.exist? on it's own will take care of all of the above for you

Resources