Config Files#
In your scripts there is often a need to store the config file, which describes the behavior of the script, like which files to delete or which servers to ping. In Ruby there is a number of possibilities to archive this. The simplest way is to just to include all the necessary information at the top of the script and, for example, store all that information in Ruby constants. But of course, this is far away from a good practice. Much better is to keep the config in a file (stored in /etc
or /usr/local/etc
for convention) and load the data from the script.
Comma-Separated Values#
The simplest possible configuration file would be a comma-separated values file, aka CSV
. Such files stores the record (row) in one line and separates the values (colums) with commas, semicolons or colons. The well-known example of the CSV-like is /etc/passwd
, which stores users, one user for one line.
Let’s imagine we want to store the servers we want to process in the script. We can write this information in servers.conf
like:
# server name, domain, server ip, server type
yoda,starwars.com,192.168.1.31,freebsd
vader,starwars.com,192.168.1.32,solaris
kirk,startrek.com,192.168.1.33,windows
It is easy to process such file, just read every single line and do neccessary stuff (like initialize the server objects). Good practice is to treat every line starting with hash sign as a comment and do not read it. Empty lines (line.chomp.empty?
) should be ignored as well. The scriplet below creates the objects (based on type
property in the config file) and adds it to the servers
array.
servers = []
open 'servers.conf' do |config_file|
config_file.each_line do |line|
unless line.chomp.empty? || line =~ /^#/
name, domain, ip, type = line.split ','
servers << case type
when /windows/i
WindowsServer.new(name, domain)
when /linux/i, /freebsd/i, /solaris/i
UnixServer.new(name, domain)
end
end
end
end
But there is no need to read CSV as a text file and extract all the information manuall. Ruby contains CSV library in the standard distribution. All you need to do is require ‘csv’
, then you can just CVS.read
the file and have an array of arrays in return. All you should do after this is to remove empty lines and comments:
require 'csv'
#=> true
servers = CSV.read 'servers.conf'
#=> [["# server name", " domain", " server ip", " server type"], ["yoda", "starwars.com", "192.168.1.31", "freebsd"], ["vader", "starwars.com", "192.168.1.32", "solaris"], ["kirk", "startrek.com", "192.168.1.33", "windows"], []]
servers = servers.reject {|x| x.empty? || x.first =~ /^#/}
#=> [["yoda", "starwars.com", "192.168.1.31", "freebsd"], ["vader", "starwars.com", "192.168.1.32", "solaris"], ["kirk", "startrek.com", "192.168.1.33", "windows"]]
There are much more possibilities with standard CSV library, you may want to take a look into a documentation with ri CSV
. One of the most useful is skip_blanks: true
which, as you probably guess, eliminates empty lines. The other interesting option is to force CSV to treat the first line as a header with header: true
option. With this, you can easily access the data with column names instead of the index numbers.
servers = CSV.read 'servers.conf', headers: true, skip_blanks: true
#=> #<CSV::Table mode:col_or_row row_count:4>
servers.headers
#=> ["# server name", " domain", " server ip", " server type"]
servers['# server name']
#=> ["yoda", "vader", "kirk"]
servers[0]['# server name']
#=> "yoda"
servers[1][' server ip'] # notice the leading space
#=> "192.168.1.32"
But header names like this are not very useful. They are Strings contaning the whitespaces - not very useful object as a key. Fortunately, CSV library has an ability to convert the header while loading the file with header_converters: converter
option. The following example will convert the headers to snake_case symbols:
servers = CSV.read 'servers.conf', headers: true, skip_blanks: true, header_converters: :symbol
servers.headers
#=> [:_server_name, :_domain, :_server_ip, :_server_type]
Better, but still not ideal - because the leading spaces convert to underscores. It would be good to have the converter which changes the header to more readable symbol. CSV library allows it - all the converters are the lambda function in CSV::HeaderConverters
hash:
CSV::HeaderConverters[:symbol]
#=> #<Proc:0x007fcfd225d790@/Users/turbo/.rbenv/versions/2.0.0-p247/lib/ruby/2.0.0/csv.rb:993 (lambda)>
When loading the file, CSV library applies the selected lambda function to every header. You can take a look into csv.rb
source code, line 993, as shown above, to learn what the function described as :symbol
is doing with the header. So why no to create our own converter? Lets call it symbolize and add to the CSV::HeaderConverters
. It should be a function which first strip
the string (remove leading and trailing spaces), then change all whitespaces to underscore (gsub(/\s+/, “”)
), then remove non-word characters, like hash (gsub(/\W+/, “”)
), remove leading underscores (gsub(/^+/, “”)
) and, finally, converts this string to symbol:
CSV::HeaderConverters[:symbolize] = lambda do |header|
header.strip.gsub(/\s+/, "_").gsub(/\W+/, "").gsub(/^_+/, "").to_sym
end
servers = CSV.read 'servers.conf', headers: true, header_converters: :symbolize,
skip_blanks: true
servers.headers
#=> [:server_name, :domain, :server_ip, :server_type]
servers[:server_name]
#=> ["yoda", "vader", "kirk"]
YAML#
YAML (YAML Ain’t Markup Language or Yet Another Markup Language) is a formal language for data serialization. Because it is language-independent and very human-readable, YAML became a standard for storing a configuration data in many frameworks and programs. YAML files are just a text files which can be read and write in any text editor. Below is an example of the data structure which may be used as a configuration file:
Serialization (aka Marshalling) is the process of translation the data structures (like Objects in Ruby) to the format, which can be stored outside the program - like in file on disk.
listener:
host: 127.0.0.1
port: 6502
machines:
- name: yoda
domain: starwars.com
- name: kirk
domain: startrek.com
This YAML can be interpreted by Ruby using YAML::load_file
method, which returns object created after parsing YAML file - in this example is it a Hash:
require 'yaml'
#=> true
YAML::load_file('server.conf')
#=> {"listener"=>{"host"=>"127.0.0.1", "port"=>6502}, "machines"=>[{"name"=>"yoda", "domain"=>"starwars.com"}, {"name"=>"kirk", "domain"=>"startrek.com"}]}
As you can see, pairs key: value
translates to Hash elements, literals became a String or a Fixnum (notice that 127.0.0.1 is a String, even if it starts with the number), and finally elements started with a dash appear as a members of an Array.
But not only Strings and Numbers can be stored: you can put every Ruby object inside the YAML file, for example Symbol
, as a literal begins with colon, Time (in specified format, like 2015-02-17 19:25:00 +0100
), true
or false
objects, or even nil
.
- :name: yoda
# you can put a comment in the YAML file
:domains: [starwars.com, star-wars.com] # Array of Strings
:golive_time: 2015-02-16 19:25:00 +01:00
:alive: true
:description:
- :name: kirk
:domain: startrek.com
:description: The old blue Sun in the corner
of the server room
The example above loads as an Array of two Hashes. Notice that you do not have to put the Array elements in distinct lines, there is a one-line shorthand for this: [element1, element2]
. Literal true
is converted to true
object, empty space became nil
. Also the date with time translates to the proper Time
object.
YAML::load_file('server.conf')
#=> [{:name=>"yoda", :domains=>["starwars.com", "star-wars.com"], :golive_time=>2015-02-16 19:25:00 +0100, :alive=>true, :description=>nil},
# {:name=>"kirk", :domain=>"startrek.com", :description=>"The old blue Sun in the corner of the server room"}]
YAML::load_file('server.conf').first[:golive_time].class
#=> Time
Saving Existing Objects to YAML#
We mentioned about serialization of the data structures. YAML allows that, which means that we can easly translate the existing object to YAML, store it, edit it, and load later.
After loading YAML library every single object can be transformed to YAML using to_yaml
method.
require 'yaml'
#=> true
servers = [{name: "kirk", no_processors: [16, 32], golive: Time.now},
{name: "yoda", no_processors: nil}]
#=> [{:name=>"kirk", :no_processors=>[16, 32], :golive=>2015-02-16 15:52:11 +0100},
# {:name=>"yoda", :no_processors=>nil}]
servers.to_yaml
#=> "---\n- :name: kirk\n :no_processors:\n - 16\n - 32\n :golive: 2015-02-16 15:52:11.242406000 +01:00\n- :name: yoda\n :no_processors: \n"
File.open('servers.conf', 'w') { |f| f.puts servers.to_yaml }
#=> nil
The to_yaml
method on this Array produces human-readable document as shown below:
---
- :name: kirk
:no_processors:
- 16
- 32
:golive: 2015-02-16 19:54:16.550614000 +01:00
- :name: yoda
:no_processors:
But not only Ruby built-in object can be stored in YAML. You can do it with every object, so your own objects as well. Consider the simple Server
objects, stored in the Array:
require 'socket' # needed for getaddress()
class Server
def initialize(name, domain)
@name = name
@domain = domain
@ip = IPSocket.getaddress("#{name}.#{domain}")
end
end
servers = [Server.new('www','rubyforadmins.com'), Server.new('www','google.com')]
servers.to_yaml
#=> "---\n- !ruby/object:Server\n name: www\n domain: rubyforadmins.com\n ip: 83.144.118.166\n- !ruby/object:Server\n name: www\n domain: google.com\n ip: 173.194.65.105\n"
After loading the following file with YAML::load_file
you wil get the Array of Server objects. That’s all!
---
- !ruby/object:Server
name: www
domain: rubyforadmins.com
ip: 83.144.118.166
- !ruby/object:Server
name: www
domain: google.com
ip: 173.194.65.105