module IniParse
module Lines
# A base class from which other line types should inherit.
module Line
# ==== Parameters
# opts<Hash>:: Extra options for the line.
#
def initialize(opts = {})
@comment = opts.fetch(:comment, nil)
@comment_sep = opts.fetch(:comment_sep, ';')
@comment_prefix = opts.fetch(:comment_prefix, ' ')
@comment_offset = opts.fetch(:comment_offset, 0)
@indent = opts.fetch(:indent, '')
@option_sep = opts.fetch(:option_sep, nil)
end
# Returns if this line has an inline comment.
def has_comment?
not @comment.nil?
end
# Returns this line as a string as it would be represented in an INI
# document.
def to_ini
[*line_contents].map { |ini|
if has_comment?
ini += ' ' if ini =~ /\S/ # not blank
ini = ini.ljust(@comment_offset)
ini += comment
end
@indent + ini
}.join "\n"
end
# Returns the contents for this line.
def line_contents
''
end
# Returns the inline comment for this line. Includes the comment
# separator at the beginning of the string.
def comment
"#{ @comment_sep }#{ @comment_prefix }#{ @comment }"
end
# Returns whether this is a line which has no data.
def blank?
false
end
# Returns the options used to create the line
def options
{
comment: @comment,
comment_sep: @comment_sep,
comment_prefix: @comment_prefix,
comment_offset: @comment_offset,
indent: @indent,
option_sep: @option_sep
}
end
end
# Represents a section header in an INI document. Section headers consist
# of a string of characters wrapped in square brackets.
#
# [section]
# key=value
# etc
# ...
#
class Section
include Line
@regex = /^\[ # Opening bracket
([^\]]+) # Section name
\]$ # Closing bracket
/x
attr_accessor :key
attr_reader :lines
include Enumerable
# ==== Parameters
# key<String>:: The section name.
# opts<Hash>:: Extra options for the line.
#
def initialize(key, opts = {})
super(opts)
@key = key.to_s
@lines = IniParse::OptionCollection.new
end
def self.parse(line, opts)
if m = @regex.match(line)
[:section, m[1], opts]
end
end
# Returns this line as a string as it would be represented in an INI
# document. Includes options, comments and blanks.
def to_ini
coll = lines.to_a
if coll.any?
[*super,coll.to_a.map do |line|
if line.kind_of?(Array)
line.map { |dup_line| dup_line.to_ini }.join($/)
else
line.to_ini
end
end].join($/)
else
super
end
end
# Enumerates through each Option in this section.
#
# Does not yield blank and comment lines by default; if you want _all_
# lines to be yielded, pass true.
#
# ==== Parameters
# include_blank<Boolean>:: Include blank/comment lines?
#
def each(*args, &blk)
@lines.each(*args, &blk)
end
# Adds a new option to this section, or updates an existing one.
#
# Note that +[]=+ has no knowledge of duplicate options and will happily
# overwrite duplicate options with your new value.
#
# section['an_option']
# # => ['duplicate one', 'duplicate two', ...]
# section['an_option'] = 'new value'
# section['an_option]
# # => 'new value'
#
# If you do not wish to overwrite duplicates, but wish instead for your
# new option to be considered a duplicate, use +add_option+ instead.
#
def []=(key, value)
line = @lines[key.to_s]
opts = {}
if line.kind_of?(Array)
opts = line.first.options
elsif line.respond_to? :options
opts = line.options
end
@lines[key.to_s] = IniParse::Lines::Option.new(key.to_s, value, opts)
end
# Returns the value of an option identified by +key+.
#
# Returns nil if there is no corresponding option. If the key provided
# matches a set of duplicate options, an array will be returned containing
# the value of each option.
#
def [](key)
key = key.to_s
if @lines.has_key?(key)
if (match = @lines[key]).kind_of?(Array)
match.map { |line| line.value }
else
match.value
end
end
end
# Deletes the option identified by +key+.
#
# Returns the section.
#
def delete(*args)
@lines.delete(*args)
self
end
# Like [], except instead of returning just the option value, it returns
# the matching line instance.
#
# Will return an array of lines if the key matches a set of duplicates.
#
def option(key)
@lines[key.to_s]
end
# Returns true if an option with the given +key+ exists in this section.
def has_option?(key)
@lines.has_key?(key.to_s)
end
# Merges section +other+ into this one. If the section being merged into
# this one contains options with the same key, they will be handled as
# duplicates.
#
# ==== Parameters
# other<IniParse::Section>:: The section to merge into this one.
#
def merge!(other)
other.lines.each(true) do |line|
if line.kind_of?(Array)
line.each { |duplicate| @lines << duplicate }
else
@lines << line
end
end
end
#######
private
#######
def line_contents
'[%s]' % key
end
end
# Stores options which appear at the beginning of a file, without a
# preceding section.
class AnonymousSection < Section
def initialize
super('__anonymous__')
end
def to_ini
# Remove the leading space which is added by joining the blank line
# content with the options.
super.gsub(/\A\n/, '')
end
#######
private
#######
def line_contents
''
end
end
# Represents probably the most common type of line in an INI document:
# an option. Consists of a key and value, usually separated with an =.
#
# key = value
#
class Option
include Line
@regex = /^\s*([^=]+?) # Option, not greedy
(\s*=\s*) # Separator, greedy
(.*?)$ # Value
/x
attr_accessor :key, :value
# ==== Parameters
# key<String>:: The option key.
# value<String>:: The value for this option.
# opts<Hash>:: Extra options for the line.
#
def initialize(key, value, opts = {})
super(opts)
@key, @value = key.to_s, value
@option_sep = opts.fetch(:option_sep, ' = ')
end
def self.parse(line, opts)
if m = @regex.match(line)
opts[:option_sep] = m[2]
[:option, m[1].strip, typecast(m[3].strip), opts]
end
end
# Attempts to typecast values.
def self.typecast(value)
case value
when /^\s*$/ then nil
when /^-?(?:\d|[1-9]\d+)$/ then Integer(value)
when /^-?(?:\d|[1-9]\d+)(?:\.\d+)?(?:e[+-]?\d+)?$/i then Float(value)
when /^true$/i then true
when /^false$/i then false
else value
end
end
#######
private
#######
# returns an array to support multiple lines or a single one at once
# because of options key duplication
def line_contents
if value.kind_of?(Array)
value.map { |v, i| "#{key}#{@option_sep}#{v}" }
else
"#{key}#{@option_sep}#{value}"
end
end
end
# Represents a blank line. Used so that we can preserve blank lines when
# writing back to the file.
class Blank
include Line
def blank?
true
end
def self.parse(line, opts)
if line !~ /\S/ # blank
if opts[:comment].nil?
[:blank]
else
[:comment, opts[:comment], opts]
end
end
end
end
# Represents a comment. Comment lines begin with a semi-colon or hash.
#
# ; this is a comment
# # also a comment
#
class Comment < Blank
# Returns if this line has an inline comment.
#
# Being a Comment this will always return true, even if the comment
# is nil. This would be the case if the line starts with a comment
# seperator, but has no comment text. See spec/fixtures/smb.ini for a
# real-world example.
#
def has_comment?
true
end
# Returns the inline comment for this line. Includes the comment
# separator at the beginning of the string.
#
# In rare cases where a comment seperator appeared in the original file,
# but without a comment, just the seperator will be returned.
#
def comment
@comment !~ /\S/ ? @comment_sep : super
end
end
end # Lines
end # IniParse