Ruby: configuration file parsing utility
Posted 04-18-2011 at 10:12 AM by catkin
Tags configuration, file, parse, parsing, ruby
Netsearching found three options for Ruby to parse configuration files:
The code generates detailed error messages if the config file is defective -- useful when creating config files.
It may be easier to understand the code when its functionality is clear so here is a commented sample configuration file:
When programming, the first thing to do is to provide a complete set of default parameters:
If a config file is to be parsed (this example assumes it can only be named on the command line):
The ParseConfigFile object receives a list of keys for validation and checks for empty values; more specific error trapping is left for the caller allowing ParseConfigFile to be a library utility:
The nitty-gritty of parsing is done by the GetConfigFileData object, called once for each configuration element:
For completeness, here is a simple example of error traps done after parsing the config file:
- YAML is powerful but I felt the the configuration file format was too complex for end-users. YAML example.
- drks' ParseConfig was not powerful enough for my needs.
- Hans' parse_config was also not powerful enough for my needs.
The code generates detailed error messages if the config file is defective -- useful when creating config files.
It may be easier to understand the code when its functionality is clear so here is a commented sample configuration file:
Code:
# * Leading and trailing whitespace is discarded. # * Lines beginning with # and empty lines are ignored. # * Lines ending in \ are continuation lines. The \ is removed and the contents # of the following line appended (any whitespace before the \ is retained). # * For string values: # - What is left must be of the format keyword = value # - Whitespace after the keyword is discarded. # - Whitespace before the value is discarded. # * For array values: # - What is left must be of the format keyword = [ # - Followed by none or more lines, each one being an array member string # These lines may be continued using a trailing \ # - Followed by a line containing a single ] # * For hash values: # - What is left must be of the format keyword = { # - Followed by none or more lines, each one being a hash key and value like # my_key => my_value # These lines may be continued using a trailing \ # - Followed by a line containing a single } CollationRootDir = /srv/docoll_dev/ Database = { host => localhost db_name => docoll_dev password => some_password port => 5432 user => superuser } # LeadingDirsToStrip regexes in config files are case-insentive LeadingDirsToStrip = [ /srv/hardcopy_indexes/ /srv/old_KT/ /srv/rsync/[^/]*/ ^home/[^/]*/ ^[A-Z]/ .*/All Users/Documents/ .*/My Documents/ .*/My Music/ .*/My Pictures/ ]
Code:
def InitialiseParameters $parameters = Hash.new # In alphabetical order ... $parameters[ "ConfigFile" ] = "" $parameters[ "CollationRootDir" ] = "/srv/docs/" $parameters[ "LeadingDirsToStrip" ] = [ \ %r|/srv/rsync/| \ ] $parameters[ "Database" ] = { \ :db_name => "collation", :host => "localhost", :password => "whatever", :port => 5432, :user => "superuser" } $parameters[ "SourceRootDirs" ] = [ "/srv/rsync/" ] end
Code:
# Parse any config file # Must do now so config file settings can be over-ridden by the command line x = ARGV.index( "--config" ) if x != nil && ARGV[ x + 1 ] != nil config_file_error_msg = ParseConfigFile( ARGV[ x + 1 ], $parameters.keys ) else config_file_error_msg = '' end
Code:
def ParseConfigFile( config_path, *valid_keywords ) # TODO: change from valid_keywords to desired_keywords? # TODO: nice to pass name of variable to load with config data rather than assuming $parameters # TODO: support "=" in value begin fd = File.open( config_path, 'r' ) rescue => error_info return "\n Config file: #{ error_info }" end error_msg = "" while true key, value, get_error_msg = GetConfigFileData( fd ) if get_error_msg == '' if key == ""; break end # Validate key valid = false valid_keywords[ 0 ].each \ do |valid_keyword| if key == valid_keyword; valid = true; break; end end if valid $parameters[ key ] = value else error_msg += "\n Invalid keyword '#{ key }' on line number #{ $INPUT_LINE_NUMBER }" end # Validate value if value == "" error_msg += "\n No value on line number #{ $INPUT_LINE_NUMBER }" end else error_msg += get_error_msg break end end if error_msg != '' error_msg = "Configuration file (#{ config_path }) error(s):" + error_msg end fd.close return error_msg end
Code:
def GetConfigFileData( fd ) # TODO: don't treat # in quoted values as a comment # TODO: allow comments after data continuing = false data = '' getting_array = false getting_hash = false key = '' begin while (line = fd.readline) line.strip! # Ignore comments and empty lines if line.index( '#' ) != nil || line == ''; next end # Gather key and value data += line case data[ -1 ] when "\\" continuing = true data.slice!( -1, 1 ) data.rstrip! next when "[" if getting_array return nil, nil, \ "\n New array started before end of array started on line" \ + array_start_line end if getting_hash return nil, nil, \ "\n Array started before end of hash started on line" \ + hash_start_line end data.slice!( -1, 1 ) data.rstrip! array_start_line = "#{ $INPUT_LINE_NUMBER }" if data[ -1 ] != "=" return nil, nil, \ "\n Array start line not of format 'key = [' on line " \ + array_start_line end getting_array = true array = Array.new when "]" if ! getting_array return nil, nil, \ + "\n Array end on line #{ $INPUT_LINE_NUMBER } before array started" end data.slice!( -1, 1 ).rstrip! if data != '' return nil, nil, \ + "\n Data invalidly given before ] on line #{ $INPUT_LINE_NUMBER }" end return key, array, "" when "{" if getting_array return nil, nil, \ "\n Hash started before end of array started on line" \ + array_start_line end if getting_hash return nil, nil, \ "\n New hash started before end of hash started on line" \ + hash_start_line end data.slice!( -1, 1 ) data.rstrip! hash_start_line = "#{ $INPUT_LINE_NUMBER }" if data[ -1 ] != "=" return nil, nil, \ "\n Hash start line not of format 'key = {' on line " \ + hash_start_line end getting_hash = true hash = Hash.new when "}" if ! getting_hash return nil, nil, \ "\n Hash end on line #{ $INPUT_LINE_NUMBER } before hash started" end data.slice!( -1, 1 ).rstrip! if data != '' return nil, nil, \ "\n Data invalidly given before } on line #{ $INPUT_LINE_NUMBER }" end return key, hash, "" end if key == '' if ! data.include?( '=' ) return nil, nil, "\n No = in #{ data }" end key, data, rest = data.split( '=' ) if rest != nil return nil, nil, "\n '=' not supported in value (" + data + rest + ')' end key.rstrip! if data == nil data = "" else data.lstrip! end end if data != nil && data != '' if getting_array array += [ data ] data = "" elsif getting_hash # TODO: would be nice to accept "key: value" too if ! data.include?( " => " ) return nil, nil, \ + "\n Hash key/value on line #{ $INPUT_LINE_NUMBER } does not have ' => '" end hash_key, hash_value = data.split( " => " ) hash = hash.merge!( { hash_key.rstrip => hash_value.lstrip } ) data = "" else return key, data, "" end end end rescue EOFError if continuing error_msg += "\n End of file found when continuation line expected" end if getting_array error_msg += "\n End of file found before end of array started on line" \ + array_start_line end if getting_hash error_msg += "\n End of file found before end of hash started on line" \ + hash_start_line end return "", "", "" end end
Code:
def CheckParameters( ) error_msg = '' error_msg += CheckDir( $parameters[ "CollationRootDir" ], 'w' ) $parameters[ "SourceRootDirs" ].each \ do |source_dir| error_msg += CheckDir( source_dir, 'r' ) end return error_msg end
Total Comments 0