#encoding: UTF-8 # This class represents the INI file and can be used to parse, modify, # and write INI files. class IniFile include Enumerable VERSION = '4.0.0' class Error < StandardError; end # Public: Open an INI file and load the contents. # # filename - The name of the file as a String # opts - The Hash of options (default: {}) # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # :continuation - Use backslash as a line contintuation # # Examples # # IniFile.load('file.ini') # #=> IniFile instance # # IniFile.load('does/not/exist.ini') # #=> nil # # Returns an IniFile instance or nil if the file could not be opened. def self.load( filename, opts = {} ) return unless File.file? filename new(opts.merge(:filename => filename)) end # Get and set the filename attr_accessor :filename # Get and set the encoding attr_accessor :encoding # Public: Create a new INI file from the given set of options. If :content # is provided then it will be used to populate the INI file. If a :filename # is provided then the contents of the file will be parsed and stored in the # INI file. If neither the :content or :filename is provided then an empty # INI file is created. # # opts - The Hash of options (default: {}) # :content - The String/Hash containing the INI contents # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # :filename - The filename as a String # :permissions - Permission bits to assign the new file # :continuation - Use backslash as a line continuation # :separator - what to output between the key, operator, and value # :force_array - Keep all values with same key in an array # # Examples # # IniFile.new # #=> an empty IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar" ) # #=> an IniFile instance # # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' ) # #=> an IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' ) # #=> an IniFile instance # # IniFile.new( :permissions => 0644 ) # #=> an IniFile instance # def initialize( opts = {} ) @comment = opts.fetch(:comment, ';#') @param = opts.fetch(:parameter, '=') @encoding = opts.fetch(:encoding, nil) @default = opts.fetch(:default, 'global') @filename = opts.fetch(:filename, nil) @permissions = opts.fetch(:permissions, nil) @continuation = opts.fetch(:continuation, true) @separator = opts.fetch(:separator, ' ') @force_array = opts.fetch(:force_array, nil) content = opts.fetch(:content, nil) @ini = Hash.new {|h,k| h[k] = Hash.new} if content.is_a?(Hash) then merge!(content) elsif content then parse(content) elsif @filename then read end end # Public: Write the contents of this IniFile to the file system. If left # unspecified, the currently configured filename and encoding will be used. # Otherwise the filename and encoding can be specified in the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # :permissions - The permission bits as a Fixnum # # Returns this IniFile instance. def write( opts = {} ) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) permissions = opts.fetch(:permissions, @permissions) mode = encoding ? "w:#{encoding}" : "w" File.open(filename, mode, permissions) do |f| @ini.each do |section,hash| f.puts "[#{section}]" hash.each {|param,val| if val.class == Hash and val.empty? f.puts param else if !val.is_a?(Array) || !@force_array f.puts "#{param}#{@separator}#{@param}#{@separator}#{escape_value val}" else val.each do |subval| f.puts "#{param}#{@separator}#{@param}#{@separator}#{escape_value subval}" end end end } f.puts end end self end alias :save :write # Public: Read the contents of the INI file from the file system and replace # and set the state of this IniFile instance. If left unspecified the # currently configured filename and encoding will be used when reading from # the file system. Otherwise the filename and encoding can be specified in # the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # # Returns this IniFile instance if the read was successful; nil is returned # if the file could not be read. def read( opts = {} ) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) return unless File.file? filename mode = encoding ? "r:#{encoding}" : "r" File.open(filename, mode) { |fd| parse fd } self end alias :restore :read # Returns this IniFile converted to a String. def to_s s = [] hash = @ini.dup default = hash.delete(@default) default.each {|param,val| s << "#{param} #{@param} #{escape_value val}"} hash.each do |section,hash| s << "[#{section}]" hash.each {|param,val| s << "#{param}#{@separator}#{@param}#{@separator}#{escape_value val}"} s << "" end s.join("\n") end # Returns this IniFile converted to a Hash. def to_h @ini.dup end # Public: Creates a copy of this inifile with the entries from the # other_inifile merged into the copy. # # other - The other IniFile. # # Returns a new IniFile. def merge( other ) self.dup.merge!(other) end # Public: Merges other_inifile into this inifile, overwriting existing # entries. Useful for having a system inifile with user overridable settings # elsewhere. # # other - The other IniFile. # # Returns this IniFile. def merge!( other ) return self if other.nil? my_keys = @ini.keys other_keys = case other when IniFile other.instance_variable_get(:@ini).keys when Hash other.keys else raise Error, "cannot merge contents from '#{other.class.name}'" end (my_keys & other_keys).each do |key| case other[key] when Hash @ini[key].merge!(other[key]) when nil nil else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end (other_keys - my_keys).each do |key| case other[key] when Hash @ini[key] = other[key].dup when nil @ini[key] = {} when String @ini[@default].merge!({key => other[key]}) else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end self end # Public: Yield each INI file section, parameter, and value in turn to the # given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section and the parameter/value pair. # # Examples # # inifile.each do |section, parameter, value| # puts "#{parameter} = #{value} [in section - #{section}]" # end # # Returns this IniFile. def each return unless block_given? @ini.each do |section,hash| hash.each do |param,val| yield section, param, val end end self end # Public: Yield each section in turn to the given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section as a Hash. # # Examples # # inifile.each_section do |section| # puts section.inspect # end # # Returns this IniFile. def each_section return unless block_given? @ini.each_key {|section| yield section} self end # Public: Remove a section identified by name from the IniFile. # # section - The section name as a String. # # Returns the deleted section Hash. def delete_section( section ) @ini.delete section.to_s end # Public: Get the section Hash by name. If the section does not exist, then # it will be created. # # section - The section name as a String. # # Examples # # inifile['global'] # #=> global section Hash # # Returns the Hash of parameter/value pairs for this section. def []( section ) return nil if section.nil? @ini[section.to_s] end # Public: Set the section to a hash of parameter/value pairs. # # section - The section name as a String. # value - The Hash of parameter/value pairs. # # Examples # # inifile['tenderloin'] = { 'gritty' => 'yes' } # #=> { 'gritty' => 'yes' } # # Returns the value Hash. def []=( section, value ) @ini[section.to_s] = value end # Public: Create a Hash containing only those INI file sections whose names # match the given regular expression. # # regex - The Regexp used to match section names. # # Examples # # inifile.match(/^tree_/) # #=> Hash of matching sections # # Return a Hash containing only those sections that match the given regular # expression. def match( regex ) @ini.dup.delete_if { |section, _| section !~ regex } end # Public: Check to see if the IniFile contains the section. # # section - The section name as a String. # # Returns true if the section exists in the IniFile. def has_section?( section ) @ini.has_key? section.to_s end # Returns an Array of section names contained in this IniFile. def sections @ini.keys end # Public: Freeze the state of this IniFile object. Any attempts to change # the object will raise an error. # # Returns this IniFile. def freeze super @ini.each_value {|h| h.freeze} @ini.freeze self end # Public: Mark this IniFile as tainted -- this will traverse each section # marking each as tainted. # # Returns this IniFile. def taint super @ini.each_value {|h| h.taint} @ini.taint self end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state of the original is copied to the duplicate. # # Returns a new IniFile. def dup other = super other.instance_variable_set(:@ini, Hash.new {|h,k| h[k] = Hash.new}) @ini.each_pair {|s,h| other[s].merge! h} other.taint if self.tainted? other end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state and the frozen state of the original is copied # to the duplicate. # # Returns a new IniFile. def clone other = dup other.freeze if self.frozen? other end # Public: Compare this IniFile to some other IniFile. For two INI files to # be equivalent, they must have the same sections with the same parameter / # value pairs in each section. # # other - The other IniFile. # # Returns true if the INI files are equivalent and false if they differ. def eql?( other ) return true if equal? other return false unless other.instance_of? self.class @ini == other.instance_variable_get(:@ini) end alias :== :eql? # Escape special characters. # # value - The String value to escape. # # Returns the escaped value. def escape_value( value ) value = value.to_s.dup value.gsub!(%r/\\([0nrt])/, '\\\\\1') value.gsub!(%r/\n/, '\n') value.gsub!(%r/\r/, '\r') value.gsub!(%r/\t/, '\t') value.gsub!(%r/\0/, '\0') value end # Parse the given content and store the information in this IniFile # instance. All data will be cleared out and replaced with the information # read from the content. # # content - A String or a file descriptor (must respond to `each_line`) # # Returns this IniFile. def parse( content ) parser = Parser.new(@ini, @param, @comment, @default, @continuation, @force_array) parser.parse(content) self end # The IniFile::Parser has the responsibility of reading the contents of an # .ini file and storing that information into a ruby Hash. The object being # parsed must respond to `each_line` - this includes Strings and any IO # object. class Parser attr_writer :section attr_accessor :property attr_accessor :value # Create a new IniFile::Parser that can be used to parse the contents of # an .ini file. # # hash - The Hash where parsed information will be stored # param - String used to separate parameter and value # comment - String containing the comment character(s) # default - The String name of the default global section # continuation - Use backslash as a line continuation character # def initialize( hash, param, comment, default, continuation, force_array ) @hash = hash @default = default @continuation = continuation @force_array = force_array comment = comment.to_s.empty? ? "\\z" : "\\s*(?:[#{comment}].*)?\\z" @section_regexp = %r/\A\s*\[([^\]]+)\]#{comment}/ @ignore_regexp = %r/\A#{comment}/ @property_regexp = %r/\A(.*?)(? true # "false" --> false # "" --> nil # "42" --> 42 # "3.14" --> 3.14 # "foo" --> "foo" # # Returns the typecast value. def typecast( value ) case value when %r/\Atrue\z/i; true when %r/\Afalse\z/i; false when %r/\A\s*\z/i; nil else stripped_value = value.strip if stripped_value =~ /^\d*\.\d+$/ Float(stripped_value) elsif stripped_value =~ /^[^0]\d*$/ Integer(stripped_value) else unescape_value(value) end end rescue unescape_value(value) end # Unescape special characters found in the value string. This will convert # escaped null, tab, carriage return, newline, and backslash into their # literal equivalents. # # value - The String value to unescape. # # Returns the unescaped value. def unescape_value( value ) value = value.to_s value.gsub!(%r/\\[0nrt\\]/) { |char| case char when '\0'; "\0" when '\n'; "\n" when '\r'; "\r" when '\t'; "\t" when '\\\\'; "\\" end } value end end end # IniFile