├── .bundle └── config ├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.rdoc ├── Rakefile ├── TODO ├── iniparse.gemspec ├── lib ├── iniparse.rb └── iniparse │ ├── document.rb │ ├── generator.rb │ ├── line_collection.rb │ ├── lines.rb │ └── parser.rb └── spec ├── document_spec.rb ├── fixture_spec.rb ├── fixtures ├── authconfig.ini ├── openttd.ini ├── race07.ini └── smb.ini ├── generator ├── method_missing_spec.rb ├── with_section_blocks_spec.rb └── without_section_blocks_spec.rb ├── iniparse_spec.rb ├── line_collection_spec.rb ├── lines_spec.rb ├── parser ├── document_parsing_spec.rb └── line_parsing_spec.rb ├── spec_fixtures.rb ├── spec_helper.rb └── spec_helper_spec.rb /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_WITHOUT: extras 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage/* 3 | doc/* 4 | pkg/* 5 | *.rbc 6 | *.swp 7 | *.gem 8 | rdoc 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: "bundle exec rspec" 3 | bundler_args: "--without extras" 4 | before_install: 5 | - gem update --system 6 | - gem update bundler 7 | - gem cleanup bundler 8 | rvm: 9 | - 2.4 10 | - 2.3 11 | - 2.2 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.5.0 2 | 3 | * OptionCollection no longer yields duplicate keys as an array, but instead yields each key in turn. 4 | 5 | For example, given an INI file: 6 | 7 | ```ini 8 | [test] 9 | a = 1 10 | a = 2 11 | b = 3 12 | ``` 13 | 14 | IniParse would previously yield a single "a" key: an array containing two `Line`s: 15 | 16 | ```ruby 17 | doc['test'].map { |line| line } 18 | # => [[, ], ] 19 | ``` 20 | 21 | Instead, each key/value pair will be yielded in turn: 22 | 23 | ```ruby 24 | doc['test'].map { |line| line } 25 | # => [, , ] 26 | ``` 27 | 28 | Directly accessing values via `[]` will still return an array of values as before: 29 | 30 | ```ruby 31 | doc['test']['a'] 32 | # => [1, 2] 33 | ``` 34 | 35 | * LineCollection#each may be called without a block, returning an Enumerator. 36 | 37 | ```ruby 38 | doc = IniParse.parse(<<~EOF) 39 | [test] 40 | a = x 41 | b = y 42 | EOF 43 | 44 | doc[test].each 45 | # => # 46 | ``` 47 | 48 | This allows for chaining as in the standard library: 49 | 50 | ```ruby 51 | doc['test'].map.with_index { |a, i| { index: i, value: a.value } } 52 | # => [{ index: 0, value: 'x' }, { index: 1, value: 'y' }] 53 | ``` 54 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2010 Anthony Williams 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = IniParse {Build Status}[http://travis-ci.org/antw/iniparse] 2 | 3 | IniParse is a pure Ruby library for parsing 4 | INI[http://en.wikipedia.org/wiki/INI_file] configuration and data 5 | files. 6 | 7 | === Main features 8 | 9 | * Support for duplicate options. While not common, some INI files 10 | contain an option more than once. IniParse does not overwrite previous 11 | options, but allows you to access all of the duplicate values. 12 | 13 | * Preservation of white space and blank lines. When writing back to 14 | your INI file, line indents, white space and comments (and their indents) 15 | are preserved. Only trailing white space (which has no significance in INI 16 | files) will be removed. 17 | 18 | * Preservation of section and option ordering. Sections and options 19 | are kept in the same order they are in the original document ensuring that 20 | nothing gets mangled when writing back to the file. 21 | 22 | If you don't need the above mentioned features, you may find the simpler 23 | IniFile gem does all you need. 24 | 25 | === Opening an INI file 26 | 27 | Parsing an INI file is fairly simple: 28 | 29 | IniParse.parse( File.read('path/to/my/file.ini') ) # => IniParse::Document 30 | 31 | IniParse.parse returns an IniParse::Document instance which represents the 32 | passed "INI string". Assuming you know the structure of the document, you can 33 | access the sections in the INI document with IniParse::Document#[]. For 34 | example: 35 | 36 | document = IniParse.parse( File.read('path/to/my/file.ini') ) 37 | 38 | document['a_section'] 39 | # => IniParse::Lines::Section 40 | 41 | document['a_section']['an_option'] 42 | # => "a value" 43 | document['a_section']['another_option'] 44 | # => "another value" 45 | 46 | In the event that duplicate options were found, an array of the values will 47 | be supplied to you. 48 | 49 | document = IniParse.parse <<-EOS 50 | [my_section] 51 | key = value 52 | key = another value 53 | key = a third value 54 | EOS 55 | 56 | document['my_section']['key'] 57 | # => ['value', 'another value', 'third value'] 58 | 59 | Options which appear before the first section will be added to a section called 60 | "__anonymous__". 61 | 62 | document = IniParse.parse <<-EOS 63 | driver = true 64 | 65 | [driver] 66 | key = value 67 | EOS 68 | 69 | document['__anonymous__']['driver'] 70 | # => true 71 | 72 | document['driver']['key'] 73 | # => 'value' 74 | 75 | === Updating an INI file 76 | 77 | document['a_section']['an_option'] 78 | # => "a value" 79 | document['a_section']['an_option'] = 'a new value' 80 | document['a_section']['an_option'] 81 | # => "a new value" 82 | 83 | Delete entire sections by calling `#delete` with the section name... 84 | 85 | document.delete('a_section') 86 | 87 | ... or a single option with `#delete` on the section object: 88 | 89 | document['a_section'].delete('an_option') 90 | 91 | === Creating a new INI file 92 | 93 | See IniParse::Generator. 94 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | require 'date' 4 | 5 | require 'rake/clean' 6 | require 'rdoc/task' 7 | require 'rspec/core/rake_task' 8 | 9 | ############################################################################## 10 | # Helpers 11 | ############################################################################## 12 | 13 | def name 14 | @name ||= Dir['*.gemspec'].first.split('.').first 15 | end 16 | 17 | def version 18 | line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/] 19 | line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1] 20 | end 21 | 22 | def date 23 | Date.today.to_s 24 | end 25 | 26 | def gemspec_file 27 | "#{name}.gemspec" 28 | end 29 | 30 | def gem_file 31 | "#{name}-#{version}.gem" 32 | end 33 | 34 | def replace_header(head, header_name) 35 | head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"} 36 | end 37 | 38 | ############################################################################## 39 | # Packaging & Installation. 40 | ############################################################################## 41 | 42 | CLEAN.include ['pkg', '*.gem', 'doc', 'coverage'] 43 | 44 | desc 'Build the gem, and push to Github' 45 | task :release => :build do 46 | unless `git branch` =~ /^\* master$/ 47 | puts "You must be on the master branch to release!" 48 | exit! 49 | end 50 | sh "git commit --allow-empty -a -m 'Release #{version}'" 51 | sh "git tag v#{version}" 52 | sh "git push origin master" 53 | sh "git push origin v#{version}" 54 | 55 | puts "Push to Rubygems.org with" 56 | puts " gem push pkg/#{name}-#{version}.gem" 57 | end 58 | 59 | desc 'Builds the IniParse gem' 60 | task :build => :gemspec do 61 | sh "mkdir -p pkg" 62 | sh "gem build #{gemspec_file}" 63 | sh "mv #{gem_file} pkg" 64 | end 65 | 66 | desc 'Creates a fresh gemspec' 67 | task :gemspec => :validate do 68 | # read spec file and split out manifest section 69 | spec = File.read(gemspec_file) 70 | head, manifest, tail = spec.split(" # = MANIFEST =\n") 71 | 72 | # replace name version and date 73 | replace_header(head, :name) 74 | replace_header(head, :version) 75 | replace_header(head, :date) 76 | 77 | # determine file list from git ls-files 78 | files = `git ls-files`. 79 | split("\n"). 80 | sort. 81 | reject { |file| file =~ /^\./ }. 82 | reject { |file| file =~ /^(rdoc|pkg|spec)/ }. 83 | map { |file| " #{file}" }. 84 | join("\n") 85 | 86 | # piece file back together and write 87 | manifest = " s.files = %w[\n#{files}\n ]\n" 88 | spec = [head, manifest, tail].join(" # = MANIFEST =\n") 89 | File.open(gemspec_file, 'w') { |io| io.write(spec) } 90 | puts "Updated #{gemspec_file}" 91 | end 92 | 93 | task :validate do 94 | libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"] 95 | unless libfiles.empty? 96 | puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir." 97 | exit! 98 | end 99 | unless Dir['VERSION*'].empty? 100 | puts "A `VERSION` file at root level violates Gem best practices." 101 | exit! 102 | end 103 | end 104 | 105 | ############################################################################## 106 | # Documentation 107 | ############################################################################## 108 | 109 | Rake::RDocTask.new do |rdoc| 110 | rdoc.rdoc_dir = 'rdoc' 111 | rdoc.title = "iniparse #{version}" 112 | rdoc.rdoc_files.include('README*') 113 | rdoc.rdoc_files.include('lib/**/*.rb') 114 | end 115 | 116 | ############################################################################## 117 | # Tests & Metrics. 118 | ############################################################################## 119 | 120 | RSpec::Core::RakeTask.new('spec') do |spec| 121 | spec.pattern = 'spec/**/*_spec.rb' 122 | end 123 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Support multi-line options with backslashes (presumably with 2 | OptionCollection). 3 | -------------------------------------------------------------------------------- /iniparse.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.specification_version = 2 if s.respond_to? :specification_version= 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 9 | s.rubygems_version = '1.3.5' 10 | 11 | ## Leave these as is they will be modified for you by the rake gemspec task. 12 | ## If your rubyforge_project name is different, then edit it and comment out 13 | ## the sub! line in the Rakefile 14 | s.name = 'iniparse' 15 | s.version = '1.5.0' 16 | s.date = '2020-02-27' 17 | 18 | s.summary = 'A pure Ruby library for parsing INI documents.' 19 | s.authors = ['Anthony Williams'] 20 | s.email = 'hi@antw.me' 21 | s.homepage = 'http://github.com/antw/iniparse' 22 | s.licenses = ['MIT'] 23 | 24 | s.description = 'A pure Ruby library for parsing INI documents. ' \ 25 | 'Preserves the structure of the original document, ' \ 26 | 'including whitespace and comments' 27 | 28 | s.require_paths = %w(lib) 29 | 30 | s.rdoc_options = ['--charset=UTF-8'] 31 | s.extra_rdoc_files = %w(LICENSE README.rdoc) 32 | 33 | # Dependencies. 34 | s.add_development_dependency('rspec', '~> 3.4') 35 | 36 | # = MANIFEST = 37 | s.files = %w[ 38 | CHANGELOG.md 39 | Gemfile 40 | LICENSE 41 | README.rdoc 42 | Rakefile 43 | TODO 44 | iniparse.gemspec 45 | lib/iniparse.rb 46 | lib/iniparse/document.rb 47 | lib/iniparse/generator.rb 48 | lib/iniparse/line_collection.rb 49 | lib/iniparse/lines.rb 50 | lib/iniparse/parser.rb 51 | ] 52 | # = MANIFEST = 53 | end 54 | -------------------------------------------------------------------------------- /lib/iniparse.rb: -------------------------------------------------------------------------------- 1 | dir = File.expand_path('iniparse', File.dirname(__FILE__)) 2 | 3 | require File.join(dir, 'document') 4 | require File.join(dir, 'generator') 5 | require File.join(dir, 'line_collection') 6 | require File.join(dir, 'lines') 7 | require File.join(dir, 'parser') 8 | 9 | module IniParse 10 | VERSION = '1.5.0'.freeze 11 | 12 | # A base class for IniParse errors. 13 | class IniParseError < StandardError; end 14 | 15 | # Raised if an error occurs parsing an INI document. 16 | class ParseError < IniParseError; end 17 | 18 | # Raised when an option line is found during parsing before the first 19 | # section. 20 | class NoSectionError < ParseError; end 21 | 22 | # Raised when a line is added to a collection which isn't allowed (e.g. 23 | # adding a Section line into an OptionCollection). 24 | class LineNotAllowed < IniParseError; end 25 | 26 | module_function 27 | 28 | # Parse given given INI document source +source+. 29 | # 30 | # See IniParse::Parser.parse 31 | # 32 | # ==== Parameters 33 | # source:: The source from the INI document. 34 | # 35 | # ==== Returns 36 | # IniParse::Document 37 | # 38 | def parse(source) 39 | IniParse::Parser.new(source.gsub(/(?:: The path to the INI document. 46 | # 47 | # ==== Returns 48 | # IniParse::Document 49 | # 50 | def open(path) 51 | document = parse(File.read(path)) 52 | document.path = path 53 | document 54 | end 55 | 56 | # Creates a new IniParse::Document using the specification you provide. 57 | # 58 | # See IniParse::Generator. 59 | # 60 | # ==== Returns 61 | # IniParse::Document 62 | # 63 | def gen(&blk) 64 | IniParse::Generator.new.gen(&blk) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/iniparse/document.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | # Represents an INI document. 3 | class Document 4 | include Enumerable 5 | 6 | attr_reader :lines 7 | attr_accessor :path 8 | 9 | # Creates a new Document instance. 10 | def initialize(path = nil) 11 | @path = path 12 | @lines = IniParse::SectionCollection.new 13 | end 14 | 15 | # Enumerates through each Section in this document. 16 | # 17 | # Does not yield blank and comment lines by default; if you want _all_ 18 | # lines to be yielded, pass true. 19 | # 20 | # ==== Parameters 21 | # include_blank:: Include blank/comment lines? 22 | # 23 | def each(*args, &blk) 24 | @lines.each(*args, &blk) 25 | end 26 | 27 | # Returns the section identified by +key+. 28 | # 29 | # Returns nil if there is no Section with the given key. 30 | # 31 | def [](key) 32 | @lines[key.to_s] 33 | end 34 | 35 | # Returns the section identified by +key+. 36 | # 37 | # If there is no Section with the given key it will be created. 38 | # 39 | def section(key) 40 | @lines[key.to_s] ||= Lines::Section.new(key.to_s) 41 | end 42 | 43 | # Deletes the section whose name matches the given +key+. 44 | # 45 | # Returns the document. 46 | # 47 | def delete(*args) 48 | @lines.delete(*args) 49 | self 50 | end 51 | 52 | # Returns this document as a string suitable for saving to a file. 53 | def to_ini 54 | string = @lines.to_a.map { |line| line.to_ini }.join($/) 55 | string = "#{ string }\n" unless string[-1] == "\n" 56 | 57 | string 58 | end 59 | 60 | alias_method :to_s, :to_ini 61 | 62 | # Returns a has representation of the INI with multi-line options 63 | # as an array 64 | def to_hash 65 | result = {} 66 | @lines.entries.each do |section| 67 | result[section.key] ||= {} 68 | section.entries.each do |option| 69 | opts = Array(option) 70 | val = opts.map { |o| o.respond_to?(:value) ? o.value : o } 71 | val = val.size > 1 ? val : val.first 72 | result[section.key][opts.first.key] = val 73 | end 74 | end 75 | result 76 | end 77 | 78 | alias_method :to_h, :to_hash 79 | 80 | # A human-readable version of the document, for debugging. 81 | def inspect 82 | sections = @lines.select { |l| l.is_a?(IniParse::Lines::Section) } 83 | "#" 84 | end 85 | 86 | # Returns true if a section with the given +key+ exists in this document. 87 | def has_section?(key) 88 | @lines.has_key?(key.to_s) 89 | end 90 | 91 | # Saves a copy of this Document to disk. 92 | # 93 | # If a path was supplied when the Document was initialized then nothing 94 | # needs to be given to Document#save. If Document was not given a file 95 | # path, or you wish to save the document elsewhere, supply a path when 96 | # calling Document#save. 97 | # 98 | # ==== Parameters 99 | # path:: A path to which this document will be saved. 100 | # 101 | # ==== Raises 102 | # IniParseError:: If your document couldn't be saved. 103 | # 104 | def save(path = nil) 105 | @path = path if path 106 | raise IniParseError, 'No path given to Document#save' if @path !~ /\S/ 107 | File.open(@path, 'w') { |f| f.write(self.to_ini) } 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/iniparse/generator.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | # Generator provides a means for easily creating new INI documents. 3 | # 4 | # Rather than trying to hack together new INI documents by manually creating 5 | # Document, Section and Option instances, it is preferable to use Generator 6 | # which will handle it all for you. 7 | # 8 | # The Generator is exposed through IniParse.gen. 9 | # 10 | # IniParse.gen do |doc| 11 | # doc.section("vehicle") do |vehicle| 12 | # vehicle.option("road_side", "left") 13 | # vehicle.option("realistic_acceleration", true) 14 | # vehicle.option("max_trains", 500) 15 | # end 16 | # 17 | # doc.section("construction") do |construction| 18 | # construction.option("build_on_slopes", true) 19 | # construction.option("autoslope", true) 20 | # end 21 | # end 22 | # 23 | # # => IniParse::Document 24 | # 25 | # This can be simplified further if you don't mind the small overhead 26 | # which comes with +method_missing+: 27 | # 28 | # IniParse.gen do |doc| 29 | # doc.vehicle do |vehicle| 30 | # vehicle.road_side = "left" 31 | # vehicle.realistic_acceleration = true 32 | # vehicle.max_trains = 500 33 | # end 34 | # 35 | # doc.construction do |construction| 36 | # construction.build_on_slopes = true 37 | # construction.autoslope = true 38 | # end 39 | # end 40 | # 41 | # # => IniParse::Document 42 | # 43 | # If you want to add slightly more complicated formatting to your document, 44 | # each line type (except blanks) takes a number of optional parameters: 45 | # 46 | # :comment:: 47 | # Adds an inline comment at the end of the line. 48 | # :comment_offset:: 49 | # Indent the comment. Measured in characters from _beginning_ of the line. 50 | # See String#ljust. 51 | # :indent:: 52 | # Adds the supplied text to the beginning of the line. 53 | # 54 | # If you supply +:indent+, +:comment_sep+, or +:comment_offset+ options when 55 | # adding a section, the same options will be inherited by all of the options 56 | # which belong to it. 57 | # 58 | # IniParse.gen do |doc| 59 | # doc.section("vehicle", 60 | # :comment => "Options for vehicles", :indent => " " 61 | # ) do |vehicle| 62 | # vehicle.option("road_side", "left") 63 | # vehicle.option("realistic_acceleration", true) 64 | # vehicle.option("max_trains", 500, :comment => "More = slower") 65 | # end 66 | # end.to_ini 67 | # 68 | # [vehicle] ; Options for vehicles 69 | # road_side = left 70 | # realistic_acceleration = true 71 | # max_trains = 500 ; More = slower 72 | # 73 | class Generator 74 | attr_reader :context 75 | attr_reader :document 76 | 77 | def initialize(opts = {}) # :nodoc: 78 | @document = IniParse::Document.new 79 | @context = @document 80 | 81 | @in_section = false 82 | @opt_stack = [opts] 83 | end 84 | 85 | def gen # :nodoc: 86 | yield self 87 | @document 88 | end 89 | 90 | # Creates a new IniParse::Document with the given sections and options. 91 | # 92 | # ==== Returns 93 | # IniParse::Document 94 | # 95 | def self.gen(opts = {}, &blk) 96 | new(opts).gen(&blk) 97 | end 98 | 99 | # Creates a new section with the given name and adds it to the document. 100 | # 101 | # You can optionally supply a block (as detailed in the documentation for 102 | # Generator#gen) in order to add options to the section. 103 | # 104 | # ==== Parameters 105 | # name:: A name for the given section. 106 | # 107 | def section(name, opts = {}) 108 | if @in_section 109 | # Nesting sections is bad, mmmkay? 110 | raise LineNotAllowed, "You can't nest sections in INI files." 111 | end 112 | 113 | # Add to a section if it already exists 114 | if @document.has_section?(name.to_s()) 115 | @context = @document[name.to_s()] 116 | else 117 | @context = Lines::Section.new(name, line_options(opts)) 118 | @document.lines << @context 119 | end 120 | 121 | if block_given? 122 | begin 123 | @in_section = true 124 | with_options(opts) { yield self } 125 | @context = @document 126 | blank() 127 | ensure 128 | @in_section = false 129 | end 130 | end 131 | end 132 | 133 | # Adds a new option to the current section. 134 | # 135 | # Can only be called as part of a section block, or after at least one 136 | # section has been added to the document. 137 | # 138 | # ==== Parameters 139 | # key:: The key (name) for this option. 140 | # value:: The option's value. 141 | # opts:: Extra options for the line (formatting, etc). 142 | # 143 | # ==== Raises 144 | # IniParse::NoSectionError:: 145 | # If no section has been added to the document yet. 146 | # 147 | def option(key, value, opts = {}) 148 | @context.lines << Lines::Option.new( 149 | key, value, line_options(opts) 150 | ) 151 | rescue LineNotAllowed 152 | # Tried to add an Option to a Document. 153 | raise NoSectionError, 154 | 'Your INI document contains an option before the first section is ' \ 155 | 'declared which is not allowed.' 156 | end 157 | 158 | # Adds a new comment line to the document. 159 | # 160 | # ==== Parameters 161 | # comment:: The text for the comment line. 162 | # 163 | def comment(comment, opts = {}) 164 | @context.lines << Lines::Comment.new( 165 | line_options(opts.merge(:comment => comment)) 166 | ) 167 | end 168 | 169 | # Adds a new blank line to the document. 170 | def blank 171 | @context.lines << Lines::Blank.new 172 | end 173 | 174 | # Wraps lines, setting default options for each. 175 | def with_options(opts = {}) # :nodoc: 176 | opts = opts.dup 177 | opts.delete(:comment) 178 | @opt_stack.push( @opt_stack.last.merge(opts)) 179 | yield self 180 | @opt_stack.pop 181 | end 182 | 183 | def method_missing(name, *args, &blk) # :nodoc: 184 | if m = name.to_s.match(/(.*)=$/) 185 | option(m[1], *args) 186 | else 187 | section(name.to_s, *args, &blk) 188 | end 189 | end 190 | 191 | ####### 192 | private 193 | ####### 194 | 195 | # Returns options for a line. 196 | # 197 | # If the context is a section, we use the section options as a base, 198 | # rather than the global defaults. 199 | # 200 | def line_options(given_opts) # :nodoc: 201 | @opt_stack.last.empty? ? given_opts : @opt_stack.last.merge(given_opts) 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/iniparse/line_collection.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | # Represents a collection of lines in an INI document. 3 | # 4 | # LineCollection acts a bit like an Array/Hash hybrid, allowing arbitrary 5 | # lines to be added to the collection, but also indexes the keys of Section 6 | # and Option lines to enable O(1) lookup via LineCollection#[]. 7 | # 8 | # The lines instances are stored in an array, +@lines+, while the index of 9 | # each Section/Option is held in a Hash, +@indicies+, keyed with the 10 | # Section/Option#key value (see LineCollection#[]=). 11 | # 12 | module LineCollection 13 | include Enumerable 14 | 15 | def initialize 16 | @lines = [] 17 | @indicies = {} 18 | end 19 | 20 | # Retrive a value identified by +key+. 21 | def [](key) 22 | has_key?(key) ? @lines[ @indicies[key] ] : nil 23 | end 24 | 25 | # Set a +value+ identified by +key+. 26 | # 27 | # If a value with the given key already exists, the value will be replaced 28 | # with the new one, with the new value taking the position of the old. 29 | # 30 | def []=(key, value) 31 | key = key.to_s 32 | 33 | if has_key?(key) 34 | @lines[ @indicies[key] ] = value 35 | else 36 | @lines << value 37 | @indicies[key] = @lines.length - 1 38 | end 39 | end 40 | 41 | # Appends a line to the collection. 42 | # 43 | # Note that if you pass a line with a key already represented in the 44 | # collection, the old item will be replaced. 45 | # 46 | def <<(line) 47 | line.blank? ? (@lines << line) : (self[line.key] = line) ; self 48 | end 49 | 50 | alias_method :push, :<< 51 | 52 | # Enumerates through the collection. 53 | # 54 | # By default #each does not yield blank and comment lines. 55 | # 56 | # ==== Parameters 57 | # include_blank:: Include blank/comment lines? 58 | # 59 | def each(include_blank = false) 60 | return enum_for(:each, include_blank) unless block_given? 61 | 62 | @lines.each do |line| 63 | if include_blank || ! (line.is_a?(Array) ? line.empty? : line.blank?) 64 | yield(line) 65 | end 66 | end 67 | end 68 | 69 | # Removes the value identified by +key+. 70 | def delete(key) 71 | key = key.key if key.respond_to?(:key) 72 | 73 | unless (idx = @indicies[key]).nil? 74 | @indicies.delete(key) 75 | @indicies.each { |k,v| @indicies[k] = v -= 1 if v > idx } 76 | @lines.delete_at(idx) 77 | end 78 | end 79 | 80 | # Returns whether +key+ is in the collection. 81 | def has_key?(*args) 82 | @indicies.has_key?(*args) 83 | end 84 | 85 | # Return an array containing the keys for the lines added to this 86 | # collection. 87 | def keys 88 | map { |line| line.key } 89 | end 90 | 91 | # Returns this collection as an array. Includes blank and comment lines. 92 | def to_a 93 | @lines.dup 94 | end 95 | 96 | # Returns this collection as a hash. Does not contain blank and comment 97 | # lines. 98 | def to_hash 99 | Hash[ *(map { |line| [line.key, line] }).flatten ] 100 | end 101 | 102 | alias_method :to_h, :to_hash 103 | end 104 | 105 | # A implementation of LineCollection used for storing (mostly) Option 106 | # instances contained within a Section. 107 | # 108 | # Since it is assumed that an INI document will only represent a section 109 | # once, if SectionCollection encounters a Section key already held in the 110 | # collection, the existing section is merged with the new one (see 111 | # IniParse::Lines::Section#merge!). 112 | class SectionCollection 113 | include LineCollection 114 | 115 | def <<(line) 116 | if line.kind_of?(IniParse::Lines::Option) || (has_key?('__anonymous__') && (line.blank? || line.kind_of?(IniParse::Lines::Comment))) 117 | option = line 118 | line = IniParse::Lines::AnonymousSection.new 119 | 120 | line.lines << option if option 121 | end 122 | 123 | if line.blank? || (! has_key?(line.key)) 124 | super # Adding a new section, comment or blank line. 125 | else 126 | self[line.key].merge!(line) 127 | end 128 | 129 | self 130 | end 131 | end 132 | 133 | # A implementation of LineCollection used for storing (mostly) Option 134 | # instances contained within a Section. 135 | # 136 | # Whenever OptionCollection encounters an Option key already held in the 137 | # collection, it treats it as a duplicate. This means that instead of 138 | # overwriting the existing value, the value is changed to an array 139 | # containing the previous _and_ the new Option instances. 140 | class OptionCollection 141 | include LineCollection 142 | 143 | # Appends a line to the collection. 144 | # 145 | # If you push an Option with a key already represented in the collection, 146 | # the previous Option will not be overwritten, but treated as a duplicate. 147 | # 148 | # ==== Parameters 149 | # line:: The line to be added to this section. 150 | # 151 | def <<(line) 152 | if line.kind_of?(IniParse::Lines::Section) 153 | raise IniParse::LineNotAllowed, 154 | "You can't add a Section to an OptionCollection." 155 | end 156 | 157 | if line.blank? || (! has_key?(line.key)) 158 | super # Adding a new option, comment or blank line. 159 | else 160 | self[line.key] = [self[line.key], line].flatten 161 | end 162 | 163 | self 164 | end 165 | 166 | def each(*args) 167 | return enum_for(:each, *args) unless block_given? 168 | 169 | super(*args) do |value| 170 | if value.is_a?(Array) 171 | value.each { |item| yield(item) } 172 | else 173 | yield value 174 | end 175 | end 176 | end 177 | 178 | # Return an array containing the keys for the lines added to this 179 | # collection. 180 | def keys 181 | map(&:key).uniq 182 | end 183 | end 184 | end 185 | -------------------------------------------------------------------------------- /lib/iniparse/lines.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | module Lines 3 | # A base class from which other line types should inherit. 4 | module Line 5 | # ==== Parameters 6 | # opts:: Extra options for the line. 7 | # 8 | def initialize(opts = {}) 9 | @comment = opts.fetch(:comment, nil) 10 | @comment_sep = opts.fetch(:comment_sep, ';') 11 | @comment_prefix = opts.fetch(:comment_prefix, ' ') 12 | @comment_offset = opts.fetch(:comment_offset, 0) 13 | @indent = opts.fetch(:indent, '') 14 | @option_sep = opts.fetch(:option_sep, nil) 15 | end 16 | 17 | # Returns if this line has an inline comment. 18 | def has_comment? 19 | not @comment.nil? 20 | end 21 | 22 | # Returns this line as a string as it would be represented in an INI 23 | # document. 24 | def to_ini 25 | [*line_contents].map { |ini| 26 | if has_comment? 27 | ini += ' ' if ini =~ /\S/ # not blank 28 | ini = ini.ljust(@comment_offset) 29 | ini += comment 30 | end 31 | @indent + ini 32 | }.join "\n" 33 | end 34 | 35 | # Returns the contents for this line. 36 | def line_contents 37 | '' 38 | end 39 | 40 | # Returns the inline comment for this line. Includes the comment 41 | # separator at the beginning of the string. 42 | def comment 43 | "#{ @comment_sep }#{ @comment_prefix }#{ @comment }" 44 | end 45 | 46 | # Returns whether this is a line which has no data. 47 | def blank? 48 | false 49 | end 50 | 51 | # Returns the options used to create the line 52 | def options 53 | { 54 | comment: @comment, 55 | comment_sep: @comment_sep, 56 | comment_prefix: @comment_prefix, 57 | comment_offset: @comment_offset, 58 | indent: @indent, 59 | option_sep: @option_sep 60 | } 61 | end 62 | end 63 | 64 | # Represents a section header in an INI document. Section headers consist 65 | # of a string of characters wrapped in square brackets. 66 | # 67 | # [section] 68 | # key=value 69 | # etc 70 | # ... 71 | # 72 | class Section 73 | include Line 74 | 75 | @regex = /^\[ # Opening bracket 76 | ([^\]]+) # Section name 77 | \]$ # Closing bracket 78 | /x 79 | 80 | attr_accessor :key 81 | attr_reader :lines 82 | 83 | include Enumerable 84 | 85 | # ==== Parameters 86 | # key:: The section name. 87 | # opts:: Extra options for the line. 88 | # 89 | def initialize(key, opts = {}) 90 | super(opts) 91 | @key = key.to_s 92 | @lines = IniParse::OptionCollection.new 93 | end 94 | 95 | def self.parse(line, opts) 96 | if m = @regex.match(line) 97 | [:section, m[1], opts] 98 | end 99 | end 100 | 101 | # Returns this line as a string as it would be represented in an INI 102 | # document. Includes options, comments and blanks. 103 | def to_ini 104 | coll = lines.to_a 105 | 106 | if coll.any? 107 | [*super,coll.to_a.map do |line| 108 | if line.kind_of?(Array) 109 | line.map { |dup_line| dup_line.to_ini }.join($/) 110 | else 111 | line.to_ini 112 | end 113 | end].join($/) 114 | else 115 | super 116 | end 117 | end 118 | 119 | # Enumerates through each Option in this section. 120 | # 121 | # Does not yield blank and comment lines by default; if you want _all_ 122 | # lines to be yielded, pass true. 123 | # 124 | # ==== Parameters 125 | # include_blank:: Include blank/comment lines? 126 | # 127 | def each(*args, &blk) 128 | @lines.each(*args, &blk) 129 | end 130 | 131 | # Adds a new option to this section, or updates an existing one. 132 | # 133 | # Note that +[]=+ has no knowledge of duplicate options and will happily 134 | # overwrite duplicate options with your new value. 135 | # 136 | # section['an_option'] 137 | # # => ['duplicate one', 'duplicate two', ...] 138 | # section['an_option'] = 'new value' 139 | # section['an_option] 140 | # # => 'new value' 141 | # 142 | # If you do not wish to overwrite duplicates, but wish instead for your 143 | # new option to be considered a duplicate, use +add_option+ instead. 144 | # 145 | def []=(key, value) 146 | line = @lines[key.to_s] 147 | opts = {} 148 | if line.kind_of?(Array) 149 | opts = line.first.options 150 | elsif line.respond_to? :options 151 | opts = line.options 152 | end 153 | @lines[key.to_s] = IniParse::Lines::Option.new(key.to_s, value, opts) 154 | end 155 | 156 | # Returns the value of an option identified by +key+. 157 | # 158 | # Returns nil if there is no corresponding option. If the key provided 159 | # matches a set of duplicate options, an array will be returned containing 160 | # the value of each option. 161 | # 162 | def [](key) 163 | key = key.to_s 164 | 165 | if @lines.has_key?(key) 166 | if (match = @lines[key]).kind_of?(Array) 167 | match.map { |line| line.value } 168 | else 169 | match.value 170 | end 171 | end 172 | end 173 | 174 | # Deletes the option identified by +key+. 175 | # 176 | # Returns the section. 177 | # 178 | def delete(*args) 179 | @lines.delete(*args) 180 | self 181 | end 182 | 183 | # Like [], except instead of returning just the option value, it returns 184 | # the matching line instance. 185 | # 186 | # Will return an array of lines if the key matches a set of duplicates. 187 | # 188 | def option(key) 189 | @lines[key.to_s] 190 | end 191 | 192 | # Returns true if an option with the given +key+ exists in this section. 193 | def has_option?(key) 194 | @lines.has_key?(key.to_s) 195 | end 196 | 197 | # Merges section +other+ into this one. If the section being merged into 198 | # this one contains options with the same key, they will be handled as 199 | # duplicates. 200 | # 201 | # ==== Parameters 202 | # other:: The section to merge into this one. 203 | # 204 | def merge!(other) 205 | other.lines.each(true) do |line| 206 | if line.kind_of?(Array) 207 | line.each { |duplicate| @lines << duplicate } 208 | else 209 | @lines << line 210 | end 211 | end 212 | end 213 | 214 | ####### 215 | private 216 | ####### 217 | 218 | def line_contents 219 | '[%s]' % key 220 | end 221 | end 222 | 223 | # Stores options which appear at the beginning of a file, without a 224 | # preceding section. 225 | class AnonymousSection < Section 226 | def initialize 227 | super('__anonymous__') 228 | end 229 | 230 | def to_ini 231 | # Remove the leading space which is added by joining the blank line 232 | # content with the options. 233 | super.gsub(/\A\n/, '') 234 | end 235 | 236 | ####### 237 | private 238 | ####### 239 | 240 | def line_contents 241 | '' 242 | end 243 | end 244 | 245 | # Represents probably the most common type of line in an INI document: 246 | # an option. Consists of a key and value, usually separated with an =. 247 | # 248 | # key = value 249 | # 250 | class Option 251 | include Line 252 | 253 | @regex = /^\s*([^=]+?) # Option, not greedy 254 | (\s*=\s*) # Separator, greedy 255 | (.*?)$ # Value 256 | /x 257 | 258 | attr_accessor :key, :value 259 | 260 | # ==== Parameters 261 | # key:: The option key. 262 | # value:: The value for this option. 263 | # opts:: Extra options for the line. 264 | # 265 | def initialize(key, value, opts = {}) 266 | super(opts) 267 | @key, @value = key.to_s, value 268 | @option_sep = opts.fetch(:option_sep, ' = ') 269 | end 270 | 271 | def self.parse(line, opts) 272 | if m = @regex.match(line) 273 | opts[:option_sep] = m[2] 274 | [:option, m[1].strip, typecast(m[3].strip), opts] 275 | end 276 | end 277 | 278 | # Attempts to typecast values. 279 | def self.typecast(value) 280 | case value 281 | when /^\s*$/ then nil 282 | when /^-?(?:\d|[1-9]\d+)$/ then Integer(value) 283 | when /^-?(?:\d|[1-9]\d+)(?:\.\d+)?(?:e[+-]?\d+)?$/i then Float(value) 284 | when /^true$/i then true 285 | when /^false$/i then false 286 | else value 287 | end 288 | end 289 | 290 | ####### 291 | private 292 | ####### 293 | 294 | # returns an array to support multiple lines or a single one at once 295 | # because of options key duplication 296 | def line_contents 297 | if value.kind_of?(Array) 298 | value.map { |v, i| "#{key}#{@option_sep}#{v}" } 299 | else 300 | "#{key}#{@option_sep}#{value}" 301 | end 302 | end 303 | end 304 | 305 | # Represents a blank line. Used so that we can preserve blank lines when 306 | # writing back to the file. 307 | class Blank 308 | include Line 309 | 310 | def blank? 311 | true 312 | end 313 | 314 | def self.parse(line, opts) 315 | if line !~ /\S/ # blank 316 | if opts[:comment].nil? 317 | [:blank] 318 | else 319 | [:comment, opts[:comment], opts] 320 | end 321 | end 322 | end 323 | end 324 | 325 | # Represents a comment. Comment lines begin with a semi-colon or hash. 326 | # 327 | # ; this is a comment 328 | # # also a comment 329 | # 330 | class Comment < Blank 331 | # Returns if this line has an inline comment. 332 | # 333 | # Being a Comment this will always return true, even if the comment 334 | # is nil. This would be the case if the line starts with a comment 335 | # seperator, but has no comment text. See spec/fixtures/smb.ini for a 336 | # real-world example. 337 | # 338 | def has_comment? 339 | true 340 | end 341 | 342 | # Returns the inline comment for this line. Includes the comment 343 | # separator at the beginning of the string. 344 | # 345 | # In rare cases where a comment seperator appeared in the original file, 346 | # but without a comment, just the seperator will be returned. 347 | # 348 | def comment 349 | @comment !~ /\S/ ? @comment_sep : super 350 | end 351 | end 352 | end # Lines 353 | end # IniParse 354 | -------------------------------------------------------------------------------- /lib/iniparse/parser.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | class Parser 3 | 4 | # Returns the line types. 5 | # 6 | # ==== Returns 7 | # Array 8 | # 9 | def self.parse_types 10 | @@parse_types ||= [] 11 | end 12 | 13 | # Sets the line types. Handy if you want to add your own custom Line 14 | # classes. 15 | # 16 | # ==== Parameters 17 | # types:: An array containing Line classes. 18 | # 19 | def self.parse_types=(types) 20 | parse_types.replace(types) 21 | end 22 | 23 | self.parse_types = [ IniParse::Lines::Section, 24 | IniParse::Lines::Option, IniParse::Lines::Blank ] 25 | 26 | # Creates a new Parser instance for parsing string +source+. 27 | # 28 | # ==== Parameters 29 | # source:: The source string. 30 | # 31 | def initialize(source) 32 | @source = source.dup 33 | end 34 | 35 | # Parses the source string and returns the resulting data structure. 36 | # 37 | # ==== Returns 38 | # IniParse::Document 39 | # 40 | def parse 41 | IniParse::Generator.gen do |generator| 42 | @source.split("\n", -1).each do |line| 43 | generator.send(*Parser.parse_line(line)) 44 | end 45 | end 46 | end 47 | 48 | class << self 49 | # Takes a raw line from an INI document, striping out any inline 50 | # comment, and indent, then returns the appropriate tuple so that the 51 | # Generator instance can add the line to the Document. 52 | # 53 | # ==== Raises 54 | # IniParse::ParseError: If the line could not be parsed. 55 | # 56 | def parse_line(line) 57 | sanitized, opts = strip_indent(*strip_comment(line, {})) 58 | 59 | parsed = nil 60 | @@parse_types.each do |type| 61 | break if (parsed = type.parse(sanitized, opts)) 62 | end 63 | 64 | if parsed.nil? 65 | raise IniParse::ParseError, 66 | "A line of your INI document could not be parsed to a " \ 67 | "LineType: #{line.inspect}." 68 | end 69 | 70 | parsed 71 | end 72 | 73 | ####### 74 | private 75 | ####### 76 | 77 | # Strips in inline comment from a line (or value), removes trailing 78 | # whitespace and sets the comment options as applicable. 79 | def strip_comment(line, opts) 80 | if m = /^(^)(?:(;|\#)(\s*)(.*))$$/.match(line) || 81 | m = /^(.*?)(?:\s+(;|\#)(\s*)(.*))$/.match(line) # Comment lines. 82 | opts[:comment] = m[4].rstrip 83 | opts[:comment_prefix] = m[3] 84 | opts[:comment_sep] = m[2] 85 | # Remove the line content (since an option value may contain a 86 | # semi-colon) _then_ get the index of the comment separator. 87 | opts[:comment_offset] = 88 | line[(m[1].length..-1)].index(m[2]) + m[1].length 89 | 90 | line = m[1] 91 | else 92 | line = line.chomp 93 | end 94 | 95 | [line, opts] 96 | end 97 | 98 | # Removes any leading whitespace from a line, and adds it to the options 99 | # hash. 100 | def strip_indent(line, opts) 101 | if m = /^(\s+).*$/.match(line) 102 | line.lstrip! 103 | opts[:indent] = m[1] 104 | end 105 | 106 | [line, opts] 107 | end 108 | end 109 | end # Parser 110 | end # IniParse 111 | -------------------------------------------------------------------------------- /spec/document_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "IniParse::Document" do 4 | it 'should have a +lines+ reader' do 5 | methods = IniParse::Document.instance_methods.map { |m| m.to_sym } 6 | expect(methods).to include(:lines) 7 | end 8 | 9 | it 'should not have a +lines+ writer' do 10 | methods = IniParse::Document.instance_methods.map { |m| m.to_sym } 11 | expect(methods).not_to include(:lines=) 12 | end 13 | 14 | it 'should delegate #[] to +lines+' do 15 | doc = IniParse::Document.new 16 | expect(doc.lines).to receive(:[]).with('key') 17 | doc['key'] 18 | end 19 | 20 | it 'should call #each to +lines+' do 21 | doc = IniParse::Document.new 22 | expect(doc.lines).to receive(:each) 23 | doc.each { |l| } 24 | end 25 | 26 | it 'should be enumerable' do 27 | expect(IniParse::Document.included_modules).to include(Enumerable) 28 | 29 | sections = [ 30 | IniParse::Lines::Section.new('first section'), 31 | IniParse::Lines::Section.new('second section') 32 | ] 33 | 34 | doc = IniParse::Document.new 35 | doc.lines << sections[0] << sections[1] 36 | 37 | expect(doc.map { |line| line }).to eq(sections) 38 | end 39 | 40 | describe '#has_section?' do 41 | before(:all) do 42 | @doc = IniParse::Document.new 43 | @doc.lines << IniParse::Lines::Section.new('first section') 44 | @doc.section('another section') 45 | end 46 | 47 | it 'should return true if a section with the given key exists' do 48 | expect(@doc).to have_section('first section') 49 | expect(@doc).to have_section('another section') 50 | end 51 | 52 | it 'should return true if no section with the given key exists' do 53 | expect(@doc).not_to have_section('second section') 54 | end 55 | end 56 | 57 | describe '#delete' do 58 | let(:document) do 59 | IniParse::Generator.gen do |doc| 60 | doc.section('first') do |section| 61 | section.alpha = 'bravo' 62 | section.charlie = 'delta' 63 | end 64 | 65 | doc.section('second') do |section| 66 | section.echo = 'foxtrot' 67 | section.golf = 'hotel' 68 | end 69 | end 70 | end 71 | 72 | it 'removes the section given a key' do 73 | expect { document.delete('first') }. 74 | to change { document['first'] }.to(nil) 75 | end 76 | 77 | it 'removes the section given a Section' do 78 | expect { document.delete(document['first']) }. 79 | to change { document['first'] }.to(nil) 80 | end 81 | 82 | it 'removes the lines' do 83 | expect { document.delete('first') }. 84 | to change { document.to_ini.match(/alpha/) }.to(nil) 85 | end 86 | 87 | it 'returns the document' do 88 | expect(document.delete('first')).to eql(document) 89 | end 90 | end 91 | 92 | describe '#to_ini' do 93 | let(:document) do 94 | IniParse.parse(<<-EOF.gsub(/^\s+/, '')) 95 | [one] 96 | alpha = bravo 97 | [two] 98 | chalie = delta 99 | EOF 100 | end 101 | 102 | context 'when the document has a trailing Blank line' do 103 | it 'outputs the newline to the output string' do 104 | expect(document.to_ini).to match(/\n\Z/) 105 | end 106 | 107 | it 'does not add a second newline to the output string' do 108 | expect(document.to_ini).to_not match(/\n\n\Z/) 109 | end 110 | end # when the document has a trailing Blank line 111 | 112 | context 'when the document has no trailing Blank line' do 113 | before { document.delete('two') } 114 | 115 | it 'adds a newline to the output string' do 116 | expect(document.to_ini).to match(/\n\Z/) 117 | end 118 | end # when the document has no trailing Blank line 119 | end # to_ini 120 | 121 | describe '#save' do 122 | describe 'when no path is given to save' do 123 | it 'should save the INI document if a path was given when initialized' do 124 | doc = IniParse::Document.new('/a/path/to/a/file.ini') 125 | expect(File).to receive(:open).with('/a/path/to/a/file.ini', 'w') 126 | doc.save 127 | end 128 | 129 | it 'should raise IniParseError if no path was given when initialized' do 130 | expect { IniParse::Document.new.save }.to \ 131 | raise_error(IniParse::IniParseError) 132 | end 133 | end 134 | 135 | describe 'when a path is given to save' do 136 | it "should update the document's +path+" do 137 | allow(File).to receive(:open).and_return(true) 138 | doc = IniParse::Document.new('/a/path/to/a/file.ini') 139 | doc.save('/a/new/path.ini') 140 | expect(doc.path).to eq('/a/new/path.ini') 141 | end 142 | 143 | it 'should save the INI document to the given path' do 144 | expect(File).to receive(:open).with('/a/new/path.ini', 'w') 145 | IniParse::Document.new('/a/path/to/a/file.ini').save('/a/new/path.ini') 146 | end 147 | 148 | it 'should raise IniParseError if no path was given when initialized' do 149 | expect { IniParse::Document.new.save }.to \ 150 | raise_error(IniParse::IniParseError) 151 | end 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/fixture_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "IniParse" do 4 | describe 'openttd.ini fixture' do 5 | before(:all) do 6 | @fixture = fixture('openttd.ini') 7 | end 8 | 9 | it 'should parse without any errors' do 10 | expect { IniParse.parse(@fixture) }.not_to raise_error 11 | end 12 | 13 | it 'should have the correct sections' do 14 | expect(IniParse.parse(fixture('openttd.ini')).lines.keys).to eq([ 15 | 'misc', 'music', 'difficulty', 'game_creation', 'vehicle', 16 | 'construction', 'station', 'economy', 'pf', 'order', 'gui', 'ai', 17 | 'locale', 'network', 'currency', 'servers', 'bans', 'news_display', 18 | 'version', 'preset-J', 'newgrf', 'newgrf-static' 19 | ]) 20 | end 21 | 22 | it 'should have the correct options' do 23 | # Test the keys from one section. 24 | doc = IniParse.parse(@fixture) 25 | section = doc['misc'] 26 | 27 | expect(section.lines.keys).to eq([ 28 | 'display_opt', 'news_ticker_sound', 'fullscreen', 'language', 29 | 'resolution', 'screenshot_format', 'savegame_format', 30 | 'rightclick_emulate', 'small_font', 'medium_font', 'large_font', 31 | 'small_size', 'medium_size', 'large_size', 'small_aa', 'medium_aa', 32 | 'large_aa', 'sprite_cache_size', 'player_face', 33 | 'transparency_options', 'transparency_locks', 'invisibility_options', 34 | 'keyboard', 'keyboard_caps' 35 | ]) 36 | 37 | # Test some of the options. 38 | expect(section['display_opt']).to eq('SHOW_TOWN_NAMES|SHOW_STATION_NAMES|SHOW_SIGNS|FULL_ANIMATION|FULL_DETAIL|WAYPOINTS') 39 | expect(section['news_ticker_sound']).to be_falsey 40 | expect(section['language']).to eq('english_US.lng') 41 | expect(section['resolution']).to eq('1680,936') 42 | expect(section['large_size']).to eq(16) 43 | 44 | # Test some other options. 45 | expect(doc['currency']['suffix']).to eq('" credits"') 46 | expect(doc['news_display']['production_nobody']).to eq('summarized') 47 | expect(doc['version']['version_number']).to eq('070039B0') 48 | 49 | expect(doc['preset-J']['gcf/1_other/BlackCC/mauvetoblackw.grf']).to be_nil 50 | expect(doc['preset-J']['gcf/1_other/OpenGFX/OpenGFX_-_newFaces_v0.1.grf']).to be_nil 51 | end 52 | 53 | it 'should be identical to the original when calling #to_ini' do 54 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 55 | end 56 | end 57 | 58 | describe 'race07.ini fixture' do 59 | before(:all) do 60 | @fixture = fixture('race07.ini') 61 | end 62 | 63 | it 'should parse without any errors' do 64 | expect { IniParse.parse(@fixture) }.not_to raise_error 65 | end 66 | 67 | it 'should have the correct sections' do 68 | expect(IniParse.parse(fixture('race07.ini')).lines.keys).to eq([ 69 | 'Header', 'Race', 'Slot010', 'Slot016', 'Slot013', 'Slot018', 70 | 'Slot002', 'END' 71 | ]) 72 | end 73 | 74 | it 'should have the correct options' do 75 | # Test the keys from one section. 76 | doc = IniParse.parse(@fixture) 77 | section = doc['Slot010'] 78 | 79 | expect(section.lines.keys).to eq([ 80 | 'Driver', 'SteamUser', 'SteamId', 'Vehicle', 'Team', 'QualTime', 81 | 'Laps', 'Lap', 'LapDistanceTravelled', 'BestLap', 'RaceTime' 82 | ]) 83 | 84 | # Test some of the options. 85 | expect(section['Driver']).to eq('Mark Voss') 86 | expect(section['SteamUser']).to eq('mvoss') 87 | expect(section['SteamId']).to eq(1865369) 88 | expect(section['Vehicle']).to eq('Chevrolet Lacetti 2007') 89 | expect(section['Team']).to eq('TEMPLATE_TEAM') 90 | expect(section['QualTime']).to eq('1:37.839') 91 | expect(section['Laps']).to eq(13) 92 | expect(section['LapDistanceTravelled']).to eq(3857.750244) 93 | expect(section['BestLap']).to eq('1:38.031') 94 | expect(section['RaceTime']).to eq('0:21:38.988') 95 | 96 | expect(section['Lap']).to eq([ 97 | '(0, -1.000, 1:48.697)', '(1, 89.397, 1:39.455)', 98 | '(2, 198.095, 1:38.060)', '(3, 297.550, 1:38.632)', 99 | '(4, 395.610, 1:38.031)', '(5, 494.242, 1:39.562)', 100 | '(6, 592.273, 1:39.950)', '(7, 691.835, 1:38.366)', 101 | '(8, 791.785, 1:39.889)', '(9, 890.151, 1:39.420)', 102 | '(10, 990.040, 1:39.401)', '(11, 1089.460, 1:39.506)', 103 | '(12, 1188.862, 1:40.017)' 104 | ]) 105 | 106 | expect(doc['Header']['Version']).to eq('1.1.1.14') 107 | expect(doc['Header']['TimeString']).to eq('2008/09/13 23:26:32') 108 | expect(doc['Header']['Aids']).to eq('0,0,0,0,0,1,1,0,0') 109 | 110 | expect(doc['Race']['AIDB']).to eq('GameData\Locations\Anderstorp_2007\2007_ANDERSTORP.AIW') 111 | expect(doc['Race']['Race Length']).to eq(0.1) 112 | end 113 | 114 | it 'should be identical to the original when calling #to_ini' do 115 | pending('awaiting presevation (or lack) of whitespace around =') 116 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 117 | end 118 | end 119 | 120 | describe 'smb.ini fixture' do 121 | before(:all) do 122 | @fixture = fixture('smb.ini') 123 | end 124 | 125 | it 'should parse without any errors' do 126 | expect { IniParse.parse(@fixture) }.not_to raise_error 127 | end 128 | 129 | it 'should have the correct sections' do 130 | expect(IniParse.parse(@fixture).lines.keys).to eq([ 131 | 'global', 'printers' 132 | ]) 133 | end 134 | 135 | it 'should have the correct options' do 136 | # Test the keys from one section. 137 | doc = IniParse.parse(@fixture) 138 | section = doc['global'] 139 | 140 | expect(section.lines.keys).to eq([ 141 | 'debug pid', 'log level', 'server string', 'printcap name', 142 | 'printing', 'encrypt passwords', 'use spnego', 'passdb backend', 143 | 'idmap domains', 'idmap config default: default', 144 | 'idmap config default: backend', 'idmap alloc backend', 145 | 'idmap negative cache time', 'map to guest', 'guest account', 146 | 'unix charset', 'display charset', 'dos charset', 'vfs objects', 147 | 'os level', 'domain master', 'max xmit', 'use sendfile', 148 | 'stream support', 'ea support', 'darwin_streams:brlm', 149 | 'enable core files', 'usershare max shares', 'usershare path', 150 | 'usershare owner only', 'usershare allow guests', 151 | 'usershare allow full config', 'com.apple:filter shares by access', 152 | 'obey pam restrictions', 'acl check permissions', 153 | 'name resolve order', 'include' 154 | ]) 155 | 156 | expect(section['display charset']).to eq('UTF-8-MAC') 157 | expect(section['vfs objects']).to eq('darwinacl,darwin_streams') 158 | expect(section['usershare path']).to eq('/var/samba/shares') 159 | end 160 | 161 | it 'should be identical to the original when calling #to_ini' do 162 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 163 | end 164 | end 165 | 166 | describe 'authconfig.ini fixture' do 167 | before(:all) do 168 | @fixture = fixture('authconfig.ini') 169 | end 170 | 171 | it 'should be identical to the original when calling #to_ini' do 172 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 173 | end 174 | end 175 | 176 | describe 'option before section fixture' do 177 | before(:all) do 178 | @fixture = fixture(:option_before_section) 179 | end 180 | 181 | it 'should be identical to the original when calling #to_ini' do 182 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 183 | end 184 | end 185 | 186 | describe 'DOS line endings' do 187 | before(:all) do 188 | @fixture = fixture(:dos_endings) 189 | end 190 | 191 | it 'should have the correct sections' do 192 | expect(IniParse.parse(@fixture).lines.keys).to eq(%w[database]) 193 | end 194 | 195 | it 'should have the correct options' do 196 | # Test the keys from one section. 197 | doc = IniParse.parse(@fixture) 198 | section = doc['database'] 199 | 200 | expect(section.lines.keys).to eq(%w[first second]) 201 | 202 | expect(section['first']).to eq(true) 203 | expect(section['second']).to eq(false) 204 | end 205 | 206 | pending 'should be identical to the original when calling #to_ini' do 207 | expect(IniParse.parse(@fixture).to_ini).to eq(@fixture) 208 | end 209 | end 210 | 211 | describe 'anonymous-order.ini fixture' do 212 | # https://github.com/antw/iniparse/issues/17 213 | let(:raw) { fixture(:anon_section_with_comments) } 214 | 215 | it 'should be identical to the original when calling #to_ini' do 216 | expect(IniParse.parse(raw).to_ini).to eq(raw) 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/fixtures/authconfig.ini: -------------------------------------------------------------------------------- 1 | [global] 2 | #--authconfig--start-line-- 3 | 4 | # Generated by authconfig on 2014/04/08 15:14:28 5 | # DO NOT EDIT THIS SECTION (delimited by --start-line--/--end-line--) 6 | # Any modification may be deleted or altered by authconfig in future 7 | 8 | template homedir = /home/%U 9 | template shell = /bin/bash 10 | winbind use default domain = true 11 | winbind offline logon = false 12 | 13 | #--authconfig--end-line-- 14 | -------------------------------------------------------------------------------- /spec/fixtures/openttd.ini: -------------------------------------------------------------------------------- 1 | 2 | [misc] 3 | display_opt = SHOW_TOWN_NAMES|SHOW_STATION_NAMES|SHOW_SIGNS|FULL_ANIMATION|FULL_DETAIL|WAYPOINTS 4 | news_ticker_sound = false 5 | fullscreen = false 6 | language = english_US.lng 7 | resolution = 1680,936 8 | screenshot_format = 9 | savegame_format = 10 | rightclick_emulate = false 11 | small_font = 12 | medium_font = 13 | large_font = 14 | small_size = 6 15 | medium_size = 10 16 | large_size = 16 17 | small_aa = false 18 | medium_aa = false 19 | large_aa = false 20 | sprite_cache_size = 4 21 | player_face = 0 22 | transparency_options = 3 23 | transparency_locks = 0 24 | invisibility_options = 30 25 | keyboard = 26 | keyboard_caps = 27 | 28 | [music] 29 | playlist = 0 30 | music_vol = 0 31 | effect_vol = 0 32 | custom_1 = 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 33 | custom_2 = 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 34 | playing = false 35 | shuffle = false 36 | extmidi = timidity 37 | 38 | [difficulty] 39 | max_no_competitors = 0 40 | competitor_start_time = 2 41 | number_towns = 1 42 | number_industries = 2 43 | max_loan = 500000 44 | initial_interest = 2 45 | vehicle_costs = 0 46 | competitor_speed = 2 47 | competitor_intelligence = 0 48 | vehicle_breakdowns = 1 49 | subsidy_multiplier = 2 50 | construction_cost = 0 51 | terrain_type = 0 52 | quantity_sea_lakes = 0 53 | economy = 0 54 | line_reverse_mode = 0 55 | disasters = 0 56 | town_council_tolerance = 0 57 | diff_level = 3 58 | 59 | [game_creation] 60 | town_name = english 61 | landscape = temperate 62 | snow_line = 56 63 | snow_line_height = 7 64 | starting_year = 1960 65 | land_generator = 1 66 | oil_refinery_limit = 32 67 | tgen_smoothness = 0 68 | generation_seed = 83252223 69 | tree_placer = 2 70 | heightmap_rotation = 0 71 | se_flat_world_height = 0 72 | map_x = 9 73 | map_y = 9 74 | 75 | [vehicle] 76 | road_side = left 77 | realistic_acceleration = true 78 | mammoth_trains = true 79 | never_expire_vehicles = true 80 | max_trains = 500 81 | max_roadveh = 500 82 | max_aircraft = 200 83 | max_ships = 300 84 | servint_ispercent = false 85 | servint_trains = 150 86 | servint_roadveh = 150 87 | servint_ships = 360 88 | servint_aircraft = 100 89 | wagon_speed_limits = true 90 | disable_elrails = false 91 | freight_trains = 1 92 | plane_speed = 4 93 | dynamic_engines = false 94 | extend_vehicle_life = 0 95 | 96 | [construction] 97 | build_on_slopes = true 98 | autoslope = true 99 | extra_dynamite = false 100 | longbridges = true 101 | signal_side = true 102 | road_stop_on_town_road = true 103 | raw_industry_construction = 1 104 | 105 | [station] 106 | always_small_airport = true 107 | join_stations = true 108 | nonuniform_stations = true 109 | station_spread = 12 110 | modified_catchment = true 111 | adjacent_stations = true 112 | 113 | [economy] 114 | town_layout = 2 115 | station_noise_level = false 116 | inflation = true 117 | multiple_industry_per_town = true 118 | same_industry_close = true 119 | bribe = true 120 | exclusive_rights = true 121 | give_money = true 122 | smooth_economy = true 123 | allow_shares = false 124 | town_growth_rate = 1 125 | larger_towns = 6 126 | initial_city_size = 2 127 | mod_road_rebuild = true 128 | dist_local_authority = 20 129 | town_noise_population = 800,2000,4000 130 | 131 | [pf] 132 | forbid_90_deg = false 133 | roadveh_queue = true 134 | pathfinder_for_trains = 2 135 | pathfinder_for_roadvehs = 2 136 | pathfinder_for_ships = 0 137 | wait_oneway_signal = 15 138 | wait_twoway_signal = 41 139 | wait_for_pbs_path = 30 140 | reserve_paths = false 141 | path_backoff_interval = 20 142 | opf.pf_maxlength = 4096 143 | opf.pf_maxdepth = 48 144 | npf.npf_max_search_nodes = 10000 145 | npf.npf_rail_firstred_penalty = 1000 146 | npf.npf_rail_firstred_exit_penalty = 10000 147 | npf.npf_rail_lastred_penalty = 1000 148 | npf.npf_rail_station_penalty = 100 149 | npf.npf_rail_slope_penalty = 100 150 | npf.npf_rail_curve_penalty = 1 151 | npf.npf_rail_depot_reverse_penalty = 5000 152 | npf.npf_rail_pbs_cross_penalty = 300 153 | npf.npf_rail_pbs_signal_back_penalty = 1500 154 | npf.npf_buoy_penalty = 200 155 | npf.npf_water_curve_penalty = 25 156 | npf.npf_road_curve_penalty = 1 157 | npf.npf_crossing_penalty = 300 158 | npf.npf_road_drive_through_penalty = 800 159 | yapf.disable_node_optimization = false 160 | yapf.max_search_nodes = 10000 161 | yapf.rail_firstred_twoway_eol = true 162 | yapf.rail_firstred_penalty = 1000 163 | yapf.rail_firstred_exit_penalty = 10000 164 | yapf.rail_lastred_penalty = 1000 165 | yapf.rail_lastred_exit_penalty = 10000 166 | yapf.rail_station_penalty = 1000 167 | yapf.rail_slope_penalty = 200 168 | yapf.rail_curve45_penalty = 300 169 | yapf.rail_curve90_penalty = 600 170 | yapf.rail_depot_reverse_penalty = 5000 171 | yapf.rail_crossing_penalty = 300 172 | yapf.rail_look_ahead_max_signals = 10 173 | yapf.rail_look_ahead_signal_p0 = 500 174 | yapf.rail_look_ahead_signal_p1 = -100 175 | yapf.rail_look_ahead_signal_p2 = 5 176 | yapf.rail_pbs_cross_penalty = 300 177 | yapf.rail_pbs_station_penalty = 800 178 | yapf.rail_pbs_signal_back_penalty = 1500 179 | yapf.rail_doubleslip_penalty = 100 180 | yapf.rail_longer_platform_penalty = 800 181 | yapf.rail_longer_platform_per_tile_penalty = 0 182 | yapf.rail_shorter_platform_penalty = 4000 183 | yapf.rail_shorter_platform_per_tile_penalty = 0 184 | yapf.road_slope_penalty = 200 185 | yapf.road_curve_penalty = 100 186 | yapf.road_crossing_penalty = 300 187 | yapf.road_stop_penalty = 800 188 | 189 | [order] 190 | gotodepot = true 191 | no_servicing_if_no_breakdowns = true 192 | timetabling = true 193 | improved_load = true 194 | selectgoods = true 195 | serviceathelipad = true 196 | gradual_loading = true 197 | 198 | [gui] 199 | colored_news_year = 1901 200 | ending_year = 2051 201 | autosave = yearly 202 | vehicle_speed = true 203 | status_long_date = true 204 | show_finances = true 205 | autoscroll = false 206 | reverse_scroll = true 207 | smooth_scroll = false 208 | measure_tooltip = true 209 | errmsg_duration = 5 210 | toolbar_pos = 1 211 | window_snap_radius = 10 212 | population_in_label = true 213 | link_terraform_toolbar = true 214 | liveries = 2 215 | prefer_teamchat = false 216 | scrollwheel_scrolling = 0 217 | scrollwheel_multiplier = 5 218 | pause_on_newgame = false 219 | advanced_vehicle_list = 2 220 | timetable_in_ticks = false 221 | loading_indicators = 1 222 | default_rail_type = 6 223 | enable_signal_gui = true 224 | drag_signals_density = 4 225 | semaphore_build_before = 1975 226 | train_income_warn = true 227 | order_review_system = 2 228 | lost_train_warn = true 229 | autorenew = true 230 | autorenew_months = 12 231 | autorenew_money = 80000 232 | always_build_infrastructure = false 233 | new_nonstop = false 234 | keep_all_autosave = false 235 | autosave_on_exit = false 236 | max_num_autosaves = 16 237 | bridge_pillars = true 238 | auto_euro = true 239 | news_message_timeout = 2 240 | show_track_reservation = false 241 | default_signal_type = 0 242 | cycle_signal_types = 0 243 | console_backlog_timeout = 100 244 | console_backlog_length = 100 245 | network_chat_box_width = 700 246 | network_chat_box_height = 25 247 | right_mouse_btn_emulation = 0 248 | 249 | [ai] 250 | ainew_active = false 251 | ai_in_multiplayer = false 252 | ai_disable_veh_train = false 253 | ai_disable_veh_roadveh = false 254 | ai_disable_veh_aircraft = false 255 | ai_disable_veh_ship = false 256 | 257 | [locale] 258 | currency = GBP 259 | units = imperial 260 | 261 | [network] 262 | max_join_time = 500 263 | pause_on_join = true 264 | server_bind_ip = 0.0.0.0 265 | server_port = 3979 266 | server_advertise = false 267 | lan_internet = 1 268 | client_name = Ant 269 | server_password = 270 | rcon_password = 271 | default_company_pass = 272 | server_name = 273 | connect_to_ip = 274 | network_id = 275 | autoclean_companies = false 276 | autoclean_unprotected = 12 277 | autoclean_protected = 36 278 | max_companies = 8 279 | max_clients = 10 280 | max_spectators = 10 281 | restart_game_year = 0 282 | min_active_clients = 0 283 | server_lang = ANY 284 | reload_cfg = false 285 | last_host = 286 | last_port = 287 | 288 | [currency] 289 | rate = 1 290 | separator = . 291 | to_euro = 0 292 | prefix = "" 293 | suffix = " credits" 294 | 295 | [servers] 296 | 297 | [bans] 298 | 299 | [news_display] 300 | arrival_player = summarized 301 | arrival_other = summarized 302 | accident = full 303 | company_info = full 304 | open = summarized 305 | close = summarized 306 | economy = summarized 307 | production_player = full 308 | production_other = full 309 | production_nobody = summarized 310 | advice = full 311 | new_vehicles = full 312 | acceptance = summarized 313 | subsidies = summarized 314 | general = summarized 315 | 316 | [version] 317 | version_string = r14768 318 | version_number = 070039B0 319 | 320 | [preset-J] 321 | gcf/1_other/BlackCC/mauvetoblackw.grf = 322 | gcf/1_other/OpenGFX/newbuildings.grf = 323 | gcf/1_other/OpenGFX/OpenGFX_NewShips_v0.1.grf = 324 | gcf/1_other/OpenGFX/OpenGFX_NewWaterFeatures.grf = 325 | gcf/1_other/OpenGFX/OpenGFX_-_newEyeCandy_v0.2.grf = 326 | gcf/1_other/OpenGFX/OpenGFX_-_newFaces_v0.1.grf = 327 | gcf/1_other/OpenGFX/OpenGFX_-_newFonts_-_Newspaper_Fonts_v0.1.grf = 328 | gcf/1_other/OpenGFX/OpenGFX_-_newFonts_-_Small_Fonts_v0.1.grf = 329 | gcf/1_other/OpenGFX/OpenGFX_-_newIndustries_v0.8.grf = 330 | gcf/1_other/OpenGFX/OpenGFX_-_newInfrastructure_-_Airports_v0.1.grf = 331 | gcf/1_other/OpenGFX/OpenGFX_-_newInfrastructure_-_newBridges_v0.1.grf = 332 | gcf/1_other/OpenGFX/OpenGFX_-_newInfrastructure_v0.6.grf = 333 | gcf/1_other/OpenGFX/OpenGFX_NewLandscape_v0.3.1.grf = 334 | gcf/1_other/OpenGFX/OpenGFX_-_newTerrain_v0.4.grf = 335 | gcf/4_infrastructure/transrapidtrackset/z/signals.grf = 336 | gcf/8_vehicles/trains_wagons/pb_ukrs/pb_ukrs.grf = 337 | gcf/8_vehicles/trains_wagons/pb_ukrs/ukrsap1w.grf = 338 | gcf/6_town_buildings/ttrs3/ttrs3w.GRF = 339 | gcf/7_stations/basic_platforms/basic_platformsw.grf = 340 | gcf/1_other/townnames/british/brit_names.grf = 341 | gcf/7_stations/indstatr/indstatrw.grf = 342 | gcf/5_industries_cargos/pikkind/pikkindw.grf = 343 | gcf/4_infrastructure/ukroadset/UKRoadsetw.grf = 344 | gcf/7_stations/newstats/newstatsw.grf = 345 | gcf/4_infrastructure/nhfoundations/nhfoundationsw.grf = 346 | gcf/4_infrastructure/bktun/BKTunw.grf = 347 | gcf/7_stations/buffers/buffers.grf = 348 | gcf/8_vehicles/planes/pb_av8/pb_av8w.grf = 349 | gcf/8_vehicles/road_vehicles/egrvts/egrvts.grf = 350 | gcf/4_infrastructure/b_newbridges/newbridgesW.GRF = 351 | gcf/8_vehicles/ships/newships/newshipsw.grf = 352 | gcf/8_vehicles/ships/newships/nshp_ecs.grf = 353 | gcf/4_infrastructure/nhdep/nhdepw.grf = 354 | gcf/4_infrastructure/fence/fencew.grf = 355 | gcf/2_landscape/stolentrees/stolentreesw_162_108.grf = 356 | gcf/5_industries_cargos/pikkind/z/pikbrikw.grf = 357 | 358 | [newgrf] 359 | gcf/2_landscape/rivers/riversw.grf = 360 | gcf/1_other/townnames/british/brit_names.grf = 361 | gcf/8_vehicles/planes/pb_av8/pb_av8w.grf = 362 | gcf/8_vehicles/trains_wagons/empty/empty.grf = 363 | gcf/8_vehicles/road_vehicles/grvts/egrvts.grf = 364 | gcf/7_stations/buffers/buffers.grf = 365 | gcf/8_vehicles/road_vehicles/heqsdt/heqsdt.grf = 366 | gcf/7_stations/indstatr/indstatrw.grf = 367 | gcf/1_other/BlackCC/mauvetoblackw.grf = 368 | gcf/4_infrastructure/b_newtramtracks/NewTramTracksW_v0.4.1.grf = 369 | gcf/4_infrastructure/nhfoundations/nhfoundationsw.grf = 370 | gcf/8_vehicles/ships/newships/newshipsw.grf = 371 | gcf/8_vehicles/ships/newships/nshp_ecs.grf = 372 | gcf/7_stations/newstats/newstatsw.grf = 373 | gcf/4_infrastructure/nhdep/nhdepw.grf = 374 | gcf/1_other/OpenGFX/morebuildings.grf = 375 | gcf/1_other/OpenGFX/newbuildings022.grf = 376 | gcf/1_other/OpenGFX/hq.grf = 377 | gcf/1_other/OpenGFX/OpenGFX_NewWaterFeatures.grf = 378 | gcf/1_other/OpenGFX/OpenGFX_-_newEyeCandy_v0.2.grf = 379 | gcf/1_other/OpenGFX/OpenGFX_NewIndustries_v0.10.1.grf = 380 | gcf/1_other/OpenGFX/OpenGFX_-_newInfrastructure_-_Airports_v0.1.grf = 381 | gcf/1_other/OpenGFX/OpenGFX_newInfrastructure_v0.7.grf = 382 | gcf/1_other/OpenGFX/OpenGFX_NewLandscape_v0.3.1.grf = 383 | gcf/1_other/OpenGFX/OpenGFX_-_newTerrain_v0.4.grf = 384 | gcf/5_industries_cargos/pikkind/pikkindw.grf = 385 | gcf/2_landscape/stolentrees/stolentreesw_162_108.grf = 386 | gcf/4_infrastructure/totalbridges/total_bridges.grf = 387 | gcf/7_stations/ukwaypoints/ukwaypointsw.grf = 388 | gcf/8_vehicles/trains_wagons/pb_ukrs/pb_ukrs.grf = 0 3 0 389 | gcf/8_vehicles/trains_wagons/pb_ukrs/ukrsap1w.grf = 390 | gcf/4_infrastructure/yarrs/yarrs.grf = 391 | gcf/6_town_buildings/w2w/w2w.grf = 392 | gcf/1_other/townnames/welsh/welshnames.grf = 393 | gcf/2_landscape/smoothsnow/smoothsnoww.grf = 394 | gcf/6_town_buildings/pb_ukrh/pb_ukrhw.grf = 395 | gcf/9_last/shiptool/shiptoolv4.grf = 396 | 397 | [newgrf-static] 398 | -------------------------------------------------------------------------------- /spec/fixtures/race07.ini: -------------------------------------------------------------------------------- 1 | ; All names and Steam IDs changed to fake values. 2 | 3 | [Header] 4 | Game=RACE 07 5 | Version=1.1.1.14 6 | TimeString=2008/09/13 23:26:32 7 | Aids=0,0,0,0,0,1,1,0,0 8 | 9 | [Race] 10 | RaceMode=5 11 | Scene=GameData\Locations\Anderstorp_2007\2007_ANDERSTORP.TRK 12 | AIDB=GameData\Locations\Anderstorp_2007\2007_ANDERSTORP.AIW 13 | Race Length=0.100 14 | Track Length=4018.8376 15 | 16 | [Slot010] 17 | Driver=Mark Voss 18 | SteamUser=mvoss 19 | SteamId=1865369 20 | Vehicle=Chevrolet Lacetti 2007 21 | Team=TEMPLATE_TEAM 22 | QualTime=1:37.839 23 | Laps=13 24 | Lap=(0, -1.000, 1:48.697) 25 | Lap=(1, 89.397, 1:39.455) 26 | Lap=(2, 198.095, 1:38.060) 27 | Lap=(3, 297.550, 1:38.632) 28 | Lap=(4, 395.610, 1:38.031) 29 | Lap=(5, 494.242, 1:39.562) 30 | Lap=(6, 592.273, 1:39.950) 31 | Lap=(7, 691.835, 1:38.366) 32 | Lap=(8, 791.785, 1:39.889) 33 | Lap=(9, 890.151, 1:39.420) 34 | Lap=(10, 990.040, 1:39.401) 35 | Lap=(11, 1089.460, 1:39.506) 36 | Lap=(12, 1188.862, 1:40.017) 37 | LapDistanceTravelled=3857.750244 38 | BestLap=1:38.031 39 | RaceTime=0:21:38.988 40 | 41 | [Slot016] 42 | Driver=Corey Ball 43 | SteamUser=cball 44 | SteamId=889853 45 | Vehicle=SEAT Leon 2007 46 | Team=TEMPLATE_TEAM 47 | QualTime=1:37.904 48 | Laps=9 49 | Lap=(0, -1.000, 1:51.434) 50 | Lap=(1, 89.397, 1:39.427) 51 | Lap=(2, 200.831, 1:38.394) 52 | Lap=(3, 300.258, 1:40.239) 53 | Lap=(4, 398.653, 1:38.172) 54 | Lap=(5, 498.891, 1:38.427) 55 | Lap=(6, 597.063, 1:39.271) 56 | Lap=(7, 695.490, 1:39.922) 57 | Lap=(8, 794.761, 1:40.305) 58 | LapDistanceTravelled=532.898193 59 | BestLap=1:38.172 60 | RaceTime=DNF 61 | Reason=0 62 | 63 | [Slot013] 64 | Driver=Gabriel Lloyd 65 | SteamUser=glloyd 66 | SteamId=635 67 | Vehicle=Chevrolet Lacetti 2007 68 | Team=TEMPLATE_TEAM 69 | QualTime=1:36.607 70 | Laps=13 71 | Lap=(0, -1.000, 1:50.816) 72 | Lap=(1, 89.397, 1:38.835) 73 | Lap=(2, 200.213, 1:38.082) 74 | Lap=(3, 299.048, 1:38.367) 75 | Lap=(4, 397.131, 1:37.825) 76 | Lap=(5, 495.497, 1:38.757) 77 | Lap=(6, 593.322, 1:38.718) 78 | Lap=(7, 692.080, 1:38.478) 79 | Lap=(8, 790.797, 1:39.347) 80 | Lap=(9, 889.275, 1:38.713) 81 | Lap=(10, 988.622, 1:38.952) 82 | Lap=(11, 1087.336, 1:39.285) 83 | Lap=(12, 1186.288, 1:40.555) 84 | LapDistanceTravelled=3898.517578 85 | BestLap=1:37.825 86 | RaceTime=0:21:36.730 87 | 88 | [Slot018] 89 | Driver=Reino Lintula 90 | SteamUser=rlintula 91 | SteamId=1816 92 | Vehicle=SEAT Leon 2007 93 | Team=TEMPLATE_TEAM 94 | QualTime=1:39.602 95 | Laps=7 96 | Lap=(0, -1.000, 1:52.788) 97 | Lap=(1, 112.593, 1:39.346) 98 | Lap=(2, 225.381, 1:40.680) 99 | Lap=(3, 324.727, 1:38.775) 100 | Lap=(4, 425.407, 1:40.149) 101 | Lap=(5, 524.182, 1:41.551) 102 | Lap=(6, 624.331, 1:46.908) 103 | LapDistanceTravelled=1333.551270 104 | BestLap=1:38.775 105 | RaceTime=DNF 106 | Reason=0 107 | 108 | [Slot002] 109 | Driver=Jerry Lalich 110 | SteamUser=jlalich 111 | SteamId=236892 112 | Vehicle=Chevrolet Lacetti 2007 113 | Team=TEMPLATE_TEAM 114 | QualTime=1:38.081 115 | Laps=13 116 | Lap=(0, -1.000, 1:48.530) 117 | Lap=(1, 89.397, 1:39.213) 118 | Lap=(2, 197.927, 1:37.933) 119 | Lap=(3, 297.140, 1:37.717) 120 | Lap=(4, 395.073, 1:38.500) 121 | Lap=(5, 492.790, 1:38.641) 122 | Lap=(6, 591.290, 1:39.810) 123 | Lap=(7, 689.931, 1:39.177) 124 | Lap=(8, 789.742, 1:40.122) 125 | Lap=(9, 888.918, 1:38.928) 126 | Lap=(10, 989.040, 1:39.238) 127 | Lap=(11, 1087.968, 1:39.223) 128 | Lap=(12, 1187.206, 1:39.888) 129 | LapDistanceTravelled=3892.720215 130 | BestLap=1:37.717 131 | RaceTime=0:21:36.920 132 | 133 | [END] 134 | -------------------------------------------------------------------------------- /spec/fixtures/smb.ini: -------------------------------------------------------------------------------- 1 | ; Configuration file for the Samba software suite. 2 | ; ============================================================================ 3 | ; 4 | ; For the format of this file and comprehensive descriptions of all the 5 | ; configuration option, please refer to the man page for smb.conf(5). 6 | ; 7 | ; The following configuration should suit most systems for basic usage and 8 | ; initial testing. It gives all clients access to their home directories and 9 | ; allows access to all printers specified in /etc/printcap. 10 | 11 | ; BEGIN required configuration 12 | 13 | ; Parameters inside the required configuration block should not be altered. 14 | ; They may be changed at any time by upgrades or other automated processes. 15 | ; 16 | ; Site-specific customizations will only be preserved if they are done 17 | ; outside this block. If you choose to make customizations, it is your 18 | ; own responsibility to verify that they work correctly with the supported 19 | ; configuration tools. 20 | 21 | [global] 22 | debug pid = yes 23 | log level = 1 24 | server string = Mac OS X 25 | 26 | printcap name = cups 27 | printing = cups 28 | 29 | encrypt passwords = yes 30 | use spnego = yes 31 | 32 | passdb backend = odsam 33 | 34 | idmap domains = default 35 | idmap config default: default = yes 36 | idmap config default: backend = odsam 37 | idmap alloc backend = odsam 38 | idmap negative cache time = 5 39 | 40 | map to guest = Bad User 41 | guest account = nobody 42 | 43 | unix charset = UTF-8-MAC 44 | display charset = UTF-8-MAC 45 | dos charset = 437 46 | 47 | vfs objects = darwinacl,darwin_streams 48 | 49 | ; Don't become a master browser unless absolutely necessary. 50 | os level = 2 51 | domain master = no 52 | 53 | ; For performance reasons, set the transmit buffer size 54 | ; to the maximum and enable sendfile support. 55 | max xmit = 131072 56 | use sendfile = yes 57 | 58 | ; The darwin_streams module gives us named streams support. 59 | stream support = yes 60 | ea support = yes 61 | 62 | ; Enable locking coherency with AFP. 63 | darwin_streams:brlm = yes 64 | 65 | ; Core files are invariably disabled system-wide, but attempting to 66 | ; dump core will trigger a crash report, so we still want to try. 67 | enable core files = yes 68 | 69 | ; Configure usershares for use by the synchronize-shares tool. 70 | usershare max shares = 1000 71 | usershare path = /var/samba/shares 72 | usershare owner only = no 73 | usershare allow guests = yes 74 | usershare allow full config = yes 75 | 76 | ; Filter inaccessible shares from the browse list. 77 | com.apple:filter shares by access = yes 78 | 79 | ; Check in with PAM to enforce SACL access policy. 80 | obey pam restrictions = yes 81 | 82 | ; Don't be trying to enforce ACLs in userspace. 83 | acl check permissions = no 84 | 85 | ; Make sure that we resolve unqualified names as NetBIOS before DNS. 86 | name resolve order = lmhosts wins bcast host 87 | 88 | ; Pull in system-wide preference settings. These are managed by 89 | ; synchronize-preferences tool. 90 | include = /var/db/smb.conf 91 | 92 | [printers] 93 | comment = All Printers 94 | path = /tmp 95 | printable = yes 96 | guest ok = no 97 | create mode = 0700 98 | writeable = no 99 | browseable = no 100 | 101 | ; Site-specific parameters can be added below this comment. 102 | ; END required configuration. 103 | -------------------------------------------------------------------------------- /spec/generator/method_missing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests use of the Generator when used like so: 4 | # 5 | # IniParse::Generator.gen do |doc| 6 | # doc.comment 'My very own comment' 7 | # doc.my_section do |section| 8 | # section.my_option = 'my value' 9 | # end 10 | # end 11 | # 12 | 13 | describe 'When generating a document using Generator with section blocks using method_missing,' do 14 | 15 | # -- 16 | # ========================================================================== 17 | # SECTION LINES 18 | # ========================================================================== 19 | # ++ 20 | 21 | describe 'adding a section' do 22 | it 'should yield an object with generator methods' do 23 | IniParse::Generator.gen do |doc| 24 | doc.a_section do |section| 25 | %w( option comment blank ).each do |meth| 26 | expect(section).to respond_to(meth) 27 | end 28 | end 29 | end 30 | end 31 | 32 | it 'should add a Section to the document' do 33 | expect(IniParse::Generator.gen do |doc| 34 | doc.a_section { |section| } 35 | end).to have_section("a_section") 36 | end 37 | 38 | it 'should change the Generator context to the section during the section block' do 39 | IniParse::Generator.gen do |doc| 40 | doc.a_section do |section| 41 | expect(section.context).to be_kind_of(IniParse::Lines::Section) 42 | expect(section.context.key).to eq("a_section") 43 | end 44 | end 45 | end 46 | 47 | it 'should reset the Generator context to the document after the section block' do 48 | IniParse::Generator.gen do |doc| 49 | doc.a_section { |section| } 50 | expect(doc.context).to be_kind_of(IniParse::Document) 51 | end 52 | end 53 | 54 | it 'should append a blank line to the document, after the section' do 55 | expect(IniParse::Generator.gen do |doc| 56 | doc.a_section { |section| } 57 | end.lines.to_a.last).to be_kind_of(IniParse::Lines::Blank) 58 | end 59 | 60 | it 'should raise a LineNotAllowed if you attempt to nest a section' do 61 | expect do 62 | IniParse::Generator.gen do |doc| 63 | doc.a_section do |section_one| 64 | section_one.another_section { |section_two| } 65 | end 66 | end 67 | end.to raise_error(IniParse::LineNotAllowed) 68 | end 69 | end 70 | 71 | # -- 72 | # ========================================================================== 73 | # OPTION LINES 74 | # ========================================================================== 75 | # ++ 76 | 77 | describe 'adding a option' do 78 | describe 'when the context is a Document' do 79 | it "adds the option to an __anonymous__ section" do 80 | doc = IniParse::Generator.gen { |doc| doc.my_option = "a value" } 81 | expect(doc['__anonymous__']['my_option']).to eql('a value') 82 | end 83 | end 84 | 85 | describe 'when the context is a Section' do 86 | it 'should add the option to the section' do 87 | document = IniParse::Generator.gen do |doc| 88 | doc.a_section do |section| 89 | section.my_option = "a value" 90 | end 91 | end 92 | 93 | section = document["a_section"] 94 | expect(section).to have_option("my_option") 95 | expect(section["my_option"]).to eq("a value") 96 | end 97 | end 98 | end 99 | 100 | # Comments and blanks are added in the same way as in the 101 | # 'with_section_block_spec.rb' specification. 102 | 103 | end 104 | -------------------------------------------------------------------------------- /spec/generator/with_section_blocks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests use of the Generator when used like so: 4 | # 5 | # IniParse::Generator.gen do |doc| 6 | # doc.comment('My very own comment') 7 | # doc.section('my_section') do |section| 8 | # section.option('my_option', 'my value') 9 | # end 10 | # end 11 | # 12 | 13 | describe 'When generating a document using Generator with section blocks,' do 14 | 15 | it 'should be able to compile an empty document' do 16 | expect { IniParse::Generator.gen { |doc| } }.not_to raise_error 17 | end 18 | 19 | it 'should raise LocalJumpError if no block is given' do 20 | expect { IniParse::Generator.gen }.to raise_error(LocalJumpError) 21 | end 22 | 23 | it "should yield an object with generator methods" do 24 | IniParse::Generator.gen do |doc| 25 | %w( section option comment blank ).each do |meth| 26 | expect(doc).to respond_to(meth) 27 | end 28 | end 29 | end 30 | 31 | # -- 32 | # ========================================================================== 33 | # SECTION LINES 34 | # ========================================================================== 35 | # ++ 36 | 37 | describe 'adding a section' do 38 | it 'should yield an object with generator methods' do 39 | IniParse::Generator.gen do |doc| 40 | doc.section("a section") do |section| 41 | %w( option comment blank ).each do |meth| 42 | expect(section).to respond_to(meth) 43 | end 44 | end 45 | end 46 | end 47 | 48 | it 'should add a Section to the document' do 49 | expect(IniParse::Generator.gen do |doc| 50 | doc.section("a section") { |section| } 51 | end).to have_section("a section") 52 | end 53 | 54 | it 'should change the Generator context to the section during the section block' do 55 | IniParse::Generator.gen do |doc| 56 | doc.section("a section") do |section| 57 | expect(section.context).to be_kind_of(IniParse::Lines::Section) 58 | expect(section.context.key).to eq("a section") 59 | end 60 | end 61 | end 62 | 63 | it 'should reset the Generator context to the document after the section block' do 64 | IniParse::Generator.gen do |doc| 65 | doc.section("a section") { |section| } 66 | expect(doc.context).to be_kind_of(IniParse::Document) 67 | end 68 | end 69 | 70 | it "should use the parent document's options as a base" do 71 | document = IniParse::Generator.gen(:indent => ' ') do |doc| 72 | doc.section("a section") { |section| } 73 | end 74 | 75 | expect(document["a section"].to_ini).to match(/\A /) 76 | end 77 | 78 | it 'should pass extra options to the Section instance' do 79 | document = IniParse::Generator.gen do |doc| 80 | doc.section("a section", :indent => ' ') { |section| } 81 | end 82 | 83 | expect(document["a section"].to_ini).to match(/\A /) 84 | end 85 | 86 | it 'should append a blank line to the document, after the section' do 87 | expect(IniParse::Generator.gen do |doc| 88 | doc.section("a section") { |section| } 89 | end.lines.to_a.last).to be_kind_of(IniParse::Lines::Blank) 90 | end 91 | 92 | it 'should raise a LineNotAllowed if you attempt to nest a section' do 93 | expect do 94 | IniParse::Generator.gen do |doc| 95 | doc.section("a section") do |section_one| 96 | section_one.section("another_section") { |section_two| } 97 | end 98 | end 99 | end.to raise_error(IniParse::LineNotAllowed) 100 | end 101 | end 102 | 103 | # -- 104 | # ========================================================================== 105 | # OPTION LINES 106 | # ========================================================================== 107 | # ++ 108 | 109 | describe 'adding a option' do 110 | 111 | describe 'when the context is a Document' do 112 | it "should add the option to an __anonymous__ section" do 113 | document = IniParse::Generator.gen do |doc| 114 | doc.option("my option", "a value") 115 | end 116 | 117 | expect(document['__anonymous__']['my option']).to eql('a value') 118 | end 119 | end 120 | 121 | describe 'when the context is a Section' do 122 | it 'should add the option to the section' do 123 | document = IniParse::Generator.gen do |doc| 124 | doc.section("a section") do |section| 125 | section.option("my option", "a value") 126 | end 127 | end 128 | 129 | section = document["a section"] 130 | expect(section).to have_option("my option") 131 | expect(section["my option"]).to eq("a value") 132 | end 133 | 134 | it 'should pass extra options to the Option instance' do 135 | document = IniParse::Generator.gen do |doc| 136 | doc.section("a section") do |section| 137 | section.option("my option", "a value", :indent => " ") 138 | end 139 | end 140 | 141 | expect(document["a section"].option("my option").to_ini).to match(/^ /) 142 | end 143 | 144 | it "should use the parent document's options as a base" do 145 | document = IniParse::Generator.gen(:indent => " ") do |doc| 146 | doc.section("a section") do |section| 147 | section.option("my option", "a value") 148 | end 149 | end 150 | 151 | expect(document["a section"].option("my option").to_ini).to match(/^ /) 152 | end 153 | 154 | it "should use the parent section's options as a base" do 155 | document = IniParse::Generator.gen do |doc| 156 | doc.section("a section", :indent => " ") do |section| 157 | section.option("my option", "a value") 158 | end 159 | end 160 | 161 | expect(document["a section"].option("my option").to_ini).to match(/^ /) 162 | end 163 | 164 | it "should allow customisation of the parent's options" do 165 | document = IniParse::Generator.gen do |doc| 166 | doc.section("a section", :indent => " ") do |section| 167 | section.option("my option", "a value", { 168 | :comment_sep => "#", :comment => 'a comment' 169 | }) 170 | end 171 | end 172 | 173 | option_ini = document["a section"].option("my option").to_ini 174 | expect(option_ini).to match(/^ /) 175 | expect(option_ini).to match(/ # a comment/) 176 | end 177 | 178 | it "should not use the parent section's comment when setting line options" do 179 | document = IniParse::Generator.gen do |doc| 180 | doc.section("a section", :comment => "My section") do |section| 181 | section.option("my option", "a value") 182 | end 183 | end 184 | 185 | expect(document["a section"].option("my option").to_ini).not_to match(/My section$/) 186 | end 187 | end 188 | end 189 | 190 | # -- 191 | # ========================================================================== 192 | # COMMENT LINES 193 | # ========================================================================== 194 | # ++ 195 | 196 | describe 'adding a comment' do 197 | it 'should pass extra options to the Option instance' do 198 | document = IniParse::Generator.gen do |doc| 199 | doc.comment("My comment", :indent => ' ') 200 | end 201 | 202 | expect(document.lines.to_a.first.to_ini).to match(/\A /) 203 | end 204 | 205 | it 'should ignore any extra :comment option' do 206 | document = IniParse::Generator.gen do |doc| 207 | doc.comment("My comment", :comment => 'Ignored') 208 | end 209 | 210 | expect(document.lines.to_a.first.to_ini).to match(/My comment/) 211 | expect(document.lines.to_a.first.to_ini).not_to match(/Ignored/) 212 | end 213 | 214 | describe 'when the context is a Document' do 215 | it 'should add a comment to the document' do 216 | document = IniParse::Generator.gen do |doc| 217 | doc.comment("My comment") 218 | end 219 | 220 | comment = document.lines.to_a.first 221 | expect(comment).to be_kind_of(IniParse::Lines::Comment) 222 | expect(comment.to_ini).to match(/My comment/) 223 | end 224 | 225 | it 'should use the default line options as a base' do 226 | document = IniParse::Generator.gen do |doc| 227 | doc.comment("My comment") 228 | end 229 | 230 | comment_ini = document.lines.to_a.first.to_ini 231 | 232 | # Match separator (;) and offset (0). 233 | expect(comment_ini).to eq('; My comment') 234 | end 235 | end 236 | 237 | describe 'when the context is a Section' do 238 | it 'should add a comment to the section' do 239 | document = IniParse::Generator.gen do |doc| 240 | doc.section("a section") do |section| 241 | section.comment("My comment") 242 | end 243 | end 244 | 245 | comment = document['a section'].lines.to_a.first 246 | expect(comment).to be_kind_of(IniParse::Lines::Comment) 247 | expect(comment.to_ini).to match(/My comment/) 248 | end 249 | 250 | it "should use the parent document's line options as a base" do 251 | document = IniParse::Generator.gen(:comment_offset => 5) do |doc| 252 | doc.section("a section") do |section| 253 | section.comment("My comment") 254 | end 255 | end 256 | 257 | expect(document['a section'].lines.to_a.first.to_ini).to match(/^ ;/) 258 | end 259 | 260 | it "should use the parent section's line options as a base" do 261 | document = IniParse::Generator.gen do |doc| 262 | doc.section("a section", :comment_offset => 5) do |section| 263 | section.comment("My comment") 264 | end 265 | end 266 | 267 | expect(document['a section'].lines.to_a.first.to_ini).to match(/^ ;/) 268 | end 269 | 270 | it "should allow customisation of the parent's options" do 271 | document = IniParse::Generator.gen do |doc| 272 | doc.section("a section", :comment_offset => 5) do |section| 273 | section.comment("My comment", :comment_sep => "#") 274 | end 275 | end 276 | 277 | # Match separator (#) and offset (5) 278 | expect(document['a section'].lines.to_a.first.to_ini).to \ 279 | eq(' # My comment') 280 | end 281 | 282 | it "should not use the parent section's comment when setting line options" do 283 | document = IniParse::Generator.gen do |doc| 284 | doc.section("a section", :comment => "My section") do |section| 285 | section.comment("My comment") 286 | end 287 | end 288 | 289 | comment_ini = document['a section'].lines.to_a.first.to_ini 290 | expect(comment_ini).to match(/My comment/) 291 | expect(comment_ini).not_to match(/My section/) 292 | end 293 | end 294 | end 295 | 296 | # -- 297 | # ========================================================================== 298 | # BLANK LINES 299 | # ========================================================================== 300 | # ++ 301 | 302 | describe 'adding a blank line' do 303 | it 'should add a blank line to the document when it is the context' do 304 | document = IniParse::Generator.gen do |doc| 305 | doc.blank 306 | end 307 | 308 | expect(document.lines.to_a.first).to be_kind_of(IniParse::Lines::Blank) 309 | end 310 | 311 | it 'should add a blank line to the section when it is the context' do 312 | document = IniParse::Generator.gen do |doc| 313 | doc.section("a section") do |section| 314 | section.blank 315 | end 316 | end 317 | 318 | expect(document['a section'].lines.to_a.first).to be_kind_of(IniParse::Lines::Blank) 319 | end 320 | end 321 | 322 | end 323 | -------------------------------------------------------------------------------- /spec/generator/without_section_blocks_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests use of the Generator when used like so: 4 | # 5 | # @gen = IniParse::Generator.new 6 | # @gen.comment('My very own comment') 7 | # @gen.section('my_section') 8 | # @gen.option('my_option', 'my value') 9 | # ... 10 | # 11 | # Or 12 | # 13 | # IniParse::Generator.gen do |doc| 14 | # doc.comment('My very own comment') 15 | # doc.section('my_section') 16 | # doc.option('my_option', 'my value') 17 | # end 18 | # 19 | 20 | describe 'When generating a document using Generator without section blocks,' do 21 | before(:each) { @gen = IniParse::Generator.new } 22 | 23 | # -- 24 | # ========================================================================== 25 | # SECTION LINES 26 | # ========================================================================== 27 | # ++ 28 | 29 | describe 'adding a section' do 30 | it 'should add a Section to the document' do 31 | @gen.section("a section") 32 | expect(@gen.document).to have_section("a section") 33 | end 34 | 35 | it 'should change the Generator context to the section' do 36 | @gen.section("a section") 37 | expect(@gen.context).to eq(@gen.document['a section']) 38 | end 39 | 40 | it 'should pass extra options to the Section instance' do 41 | @gen.section("a section", :indent => ' ') 42 | expect(@gen.document["a section"].to_ini).to match(/\A /) 43 | end 44 | end 45 | 46 | # -- 47 | # ========================================================================== 48 | # OPTION LINES 49 | # ========================================================================== 50 | # ++ 51 | 52 | describe 'adding a option' do 53 | it 'should pass extra options to the Option instance' do 54 | @gen.section("a section") 55 | @gen.option("my option", "a value", :indent => ' ') 56 | expect(@gen.document["a section"].option("my option").to_ini).to match(/^ /) 57 | end 58 | 59 | describe 'when the context is a Document' do 60 | it "should add the option to an __anonymous__ section" do 61 | @gen.option("key", "value") 62 | expect(@gen.document['__anonymous__']['key']).to eql('value') 63 | end 64 | end 65 | 66 | describe 'when the context is a Section' do 67 | it 'should add the option to the section' do 68 | @gen.section("a section") 69 | @gen.option("my option", "a value") 70 | expect(@gen.document["a section"]).to have_option("my option") 71 | expect(@gen.document["a section"]["my option"]).to eq("a value") 72 | end 73 | end 74 | end 75 | 76 | # -- 77 | # ========================================================================== 78 | # COMMENT LINES 79 | # ========================================================================== 80 | # ++ 81 | 82 | describe 'adding a comment' do 83 | it 'should pass extra options to the Option instance' do 84 | @gen.comment("My comment", :indent => ' ') 85 | expect(@gen.document.lines.to_a.first.to_ini).to match(/^ /) 86 | end 87 | 88 | it 'should ignore any extra :comment option' do 89 | @gen.comment("My comment", :comment => 'Ignored') 90 | comment_ini = @gen.document.lines.to_a.first.to_ini 91 | expect(comment_ini).to match(/My comment/) 92 | expect(comment_ini).not_to match(/Ignored/) 93 | end 94 | 95 | describe 'when the context is a Document' do 96 | it 'should add a comment to the document' do 97 | @gen.comment('My comment') 98 | comment = @gen.document.lines.to_a.first 99 | expect(comment).to be_kind_of(IniParse::Lines::Comment) 100 | expect(comment.to_ini).to match(/; My comment/) 101 | end 102 | end 103 | 104 | describe 'when the context is a Section' do 105 | it 'should add a comment to the section' do 106 | @gen.section('a section') 107 | @gen.comment('My comment') 108 | comment = @gen.document['a section'].lines.to_a.first 109 | expect(comment).to be_kind_of(IniParse::Lines::Comment) 110 | expect(comment.to_ini).to match(/My comment/) 111 | end 112 | end 113 | end 114 | 115 | # -- 116 | # ========================================================================== 117 | # BLANK LINES 118 | # ========================================================================== 119 | # ++ 120 | 121 | describe 'adding a blank line' do 122 | it 'should add a blank line to the document when it is the context' do 123 | @gen.blank 124 | comment = @gen.document.lines.to_a.first 125 | expect(comment).to be_kind_of(IniParse::Lines::Blank) 126 | end 127 | 128 | it 'should add a blank line to the section when it is the context' do 129 | @gen.section('a section') 130 | @gen.blank 131 | comment = @gen.document['a section'].lines.to_a.first 132 | expect(comment).to be_kind_of(IniParse::Lines::Blank) 133 | end 134 | end 135 | 136 | end 137 | -------------------------------------------------------------------------------- /spec/iniparse_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe "IniParse" do 5 | describe '.parse' do 6 | context 'with a line ending in a backslash' do 7 | let(:ini) do 8 | <<-'INI'.gsub(/^\s*/, '') 9 | [a] 10 | opt = 1 \ 11 | other = 2 12 | INI 13 | end 14 | 15 | let(:doc) { IniParse.parse(ini) } 16 | 17 | it 'recognises the line continuation' do 18 | expect(doc.to_s).to eq("[a]\nopt = 1 other = 2\n") 19 | end 20 | 21 | it 'has one option' do 22 | expect(doc['a'].to_a.length).to eq(1) 23 | end 24 | end 25 | 26 | context 'with a line ending in a double-backslash' do 27 | let(:ini) do 28 | <<-'INI'.gsub(/^\s*/, '') 29 | [a] 30 | opt = 1 \\ 31 | other = 2 32 | INI 33 | end 34 | 35 | let(:doc) { IniParse.parse(ini) } 36 | 37 | it 'does not see a line continuation' do 38 | expect(doc.to_s).to eq(ini) 39 | end 40 | 41 | it 'has one option' do 42 | expect(doc['a'].to_a.length).to eq(2) 43 | end 44 | end 45 | end 46 | 47 | describe '.open' do 48 | before(:each) { allow(File).to receive(:read).and_return('[section]') } 49 | 50 | it 'should return an IniParse::Document' do 51 | expect(IniParse.open('/my/path.ini')).to be_kind_of(IniParse::Document) 52 | end 53 | 54 | it 'should set the path on the returned Document' do 55 | expect(IniParse.open('/my/path.ini').path).to eq('/my/path.ini') 56 | end 57 | 58 | it 'should read the file at the given path' do 59 | expect(File).to receive(:read).with('/my/path.ini').and_return('[section]') 60 | IniParse.open('/my/path.ini') 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/line_collection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # ---------------------------------------------------------------------------- 4 | # Shared specs for all Collection types... 5 | # ---------------------------------------------------------------------------- 6 | 7 | shared_examples_for "LineCollection" do 8 | before(:each) do 9 | @collection << (@c1 = IniParse::Lines::Comment.new) 10 | @collection << @i1 11 | @collection << @i2 12 | @collection << (@b1 = IniParse::Lines::Blank.new) 13 | @collection << @i3 14 | @collection << (@b2 = IniParse::Lines::Blank.new) 15 | end 16 | 17 | describe '#each' do 18 | it 'should remove blanks and comments by default' do 19 | @collection.each { |l| expect(l).to be_kind_of(@i1.class) } 20 | end 21 | 22 | it 'should not remove blanks and comments if true is given' do 23 | arr = [] 24 | 25 | # map(true)->each(true) not possible with Enumerable 26 | @collection.each(true) do |line| 27 | arr << line 28 | end 29 | 30 | expect(arr).to eq([@c1, @i1, @i2, @b1, @i3, @b2]) 31 | end 32 | end 33 | 34 | describe '#[]' do 35 | it 'should fetch the correct value' do 36 | expect(@collection['first']).to eq(@i1) 37 | expect(@collection['second']).to eq(@i2) 38 | expect(@collection['third']).to eq(@i3) 39 | end 40 | 41 | it 'should return nil if the given key does not exist' do 42 | expect(@collection['does not exist']).to be_nil 43 | end 44 | end 45 | 46 | describe '#[]=' do 47 | it 'should successfully add a new key' do 48 | @collection['fourth'] = @new 49 | expect(@collection['fourth']).to eq(@new) 50 | end 51 | 52 | it 'should successfully update an existing key' do 53 | @collection['second'] = @new 54 | expect(@collection['second']).to eq(@new) 55 | 56 | # Make sure the old data is gone. 57 | expect(@collection.detect { |s| s.key == 'second' }).to be_nil 58 | end 59 | 60 | it 'should typecast given keys to a string' do 61 | @collection[:a_symbol] = @new 62 | expect(@collection['a_symbol']).to eq(@new) 63 | end 64 | end 65 | 66 | describe '#<<' do 67 | it 'should set the key correctly if given a new item' do 68 | expect(@collection).not_to have_key(@new.key) 69 | @collection << @new 70 | expect(@collection).to have_key(@new.key) 71 | end 72 | 73 | it 'should append Blank lines' do 74 | @collection << IniParse::Lines::Blank.new 75 | expect(@collection.instance_variable_get(:@lines).last).to \ 76 | be_kind_of(IniParse::Lines::Blank) 77 | end 78 | 79 | it 'should append Comment lines' do 80 | @collection << IniParse::Lines::Comment.new 81 | expect(@collection.instance_variable_get(:@lines).last).to \ 82 | be_kind_of(IniParse::Lines::Comment) 83 | end 84 | 85 | it 'should return self' do 86 | expect(@collection << @new).to eq(@collection) 87 | end 88 | end 89 | 90 | describe '#delete' do 91 | it 'should remove the given value and adjust the indicies' do 92 | expect(@collection['second']).not_to be_nil 93 | @collection.delete('second') 94 | expect(@collection['second']).to be_nil 95 | expect(@collection['first']).to eq(@i1) 96 | expect(@collection['third']).to eq(@i3) 97 | end 98 | 99 | it "should do nothing if the supplied key does not exist" do 100 | @collection.delete('does not exist') 101 | expect(@collection['first']).to eq(@i1) 102 | expect(@collection['third']).to eq(@i3) 103 | end 104 | end 105 | 106 | describe '#to_a' do 107 | it 'should return an array' do 108 | expect(@collection.to_a).to be_kind_of(Array) 109 | end 110 | 111 | it 'should include all lines' do 112 | expect(@collection.to_a).to eq([@c1, @i1, @i2, @b1, @i3, @b2]) 113 | end 114 | 115 | it 'should include references to the same line objects as the collection' do 116 | @collection << @new 117 | expect(@collection.to_a.last.object_id).to eq(@new.object_id) 118 | end 119 | end 120 | 121 | describe '#to_hash' do 122 | it 'should return a hash' do 123 | expect(@collection.to_hash).to be_kind_of(Hash) 124 | end 125 | 126 | it 'should have the correct keys' do 127 | hash = @collection.to_hash 128 | expect(hash.keys.length).to eq(3) 129 | expect(hash).to have_key('first') 130 | expect(hash).to have_key('second') 131 | expect(hash).to have_key('third') 132 | end 133 | 134 | it 'should have the correct values' do 135 | hash = @collection.to_hash 136 | expect(hash['first']).to eq(@i1) 137 | expect(hash['second']).to eq(@i2) 138 | expect(hash['third']).to eq(@i3) 139 | end 140 | end 141 | 142 | describe '#keys' do 143 | it 'should return an array of strings' do 144 | expect(@collection.keys).to eq(['first', 'second', 'third']) 145 | end 146 | end 147 | end 148 | 149 | # ---------------------------------------------------------------------------- 150 | # On with the collection specs... 151 | # ---------------------------------------------------------------------------- 152 | 153 | describe 'IniParse::OptionCollection' do 154 | before(:each) do 155 | @collection = IniParse::OptionCollection.new 156 | @i1 = IniParse::Lines::Option.new('first', 'v1') 157 | @i2 = IniParse::Lines::Option.new('second', 'v2') 158 | @i3 = IniParse::Lines::Option.new('third', 'v3') 159 | @new = IniParse::Lines::Option.new('fourth', 'v4') 160 | end 161 | 162 | it_should_behave_like 'LineCollection' 163 | 164 | describe '#<<' do 165 | it 'should raise a LineNotAllowed exception if a Section is pushed' do 166 | expect { @collection << IniParse::Lines::Section.new('s') }.to \ 167 | raise_error(IniParse::LineNotAllowed) 168 | end 169 | 170 | it 'should add the Option as a duplicate if an option with the same key exists' do 171 | option_one = IniParse::Lines::Option.new('k', 'value one') 172 | option_two = IniParse::Lines::Option.new('k', 'value two') 173 | 174 | @collection << option_one 175 | @collection << option_two 176 | 177 | expect(@collection['k']).to eq([option_one, option_two]) 178 | end 179 | end 180 | 181 | describe '#keys' do 182 | it 'should handle duplicates' do 183 | @collection << @i1 << @i2 << @i3 184 | @collection << IniParse::Lines::Option.new('first', 'v5') 185 | expect(@collection.keys).to eq(['first', 'second', 'third']) 186 | end 187 | end 188 | 189 | context 'with duplicate lines' do 190 | let(:collection) { IniParse::OptionCollection.new } 191 | 192 | let(:opt_1) { IniParse::Lines::Option.new('my_key', 'value_1') } 193 | let(:opt_2) { IniParse::Lines::Option.new('my_key', 'value_2') } 194 | 195 | before do 196 | collection << opt_1 197 | collection << opt_2 198 | end 199 | 200 | it 'yields each item in turn' do 201 | expect { |b| collection.each(&b) }.to yield_successive_args(opt_1, opt_2) 202 | end 203 | end 204 | end 205 | 206 | describe 'IniParse::SectionCollection' do 207 | before(:each) do 208 | @collection = IniParse::SectionCollection.new 209 | @i1 = IniParse::Lines::Section.new('first') 210 | @i2 = IniParse::Lines::Section.new('second') 211 | @i3 = IniParse::Lines::Section.new('third') 212 | @new = IniParse::Lines::Section.new('fourth') 213 | end 214 | 215 | it_should_behave_like 'LineCollection' 216 | 217 | describe '#<<' do 218 | it 'should add merge Section with the other, if it is a duplicate' do 219 | new_section = IniParse::Lines::Section.new('first') 220 | @collection << @i1 221 | expect(@i1).to receive(:merge!).with(new_section).once 222 | @collection << new_section 223 | end 224 | end 225 | end 226 | -------------------------------------------------------------------------------- /spec/lines_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module IniParse::Test 4 | class FakeLine 5 | include IniParse::Lines::Line 6 | 7 | def line_contents 8 | 'fake line' 9 | end 10 | end 11 | end 12 | 13 | describe "IniParse::Lines::Line module" do 14 | describe '#to_ini' do 15 | it 'should return +line_contents+' do 16 | expect(IniParse::Test::FakeLine.new.to_ini).to eq('fake line') 17 | end 18 | 19 | it 'should preserve line indents' do 20 | expect(IniParse::Test::FakeLine.new( 21 | :indent => ' ' 22 | ).to_ini).to eq(' fake line') 23 | end 24 | 25 | describe 'when a comment is set' do 26 | it 'should correctly include the comment' do 27 | expect(IniParse::Test::FakeLine.new( 28 | :comment => 'comment', :comment_sep => ';', :comment_offset => 10 29 | ).to_ini).to eq('fake line ; comment') 30 | end 31 | 32 | it 'should correctly indent the comment' do 33 | expect(IniParse::Test::FakeLine.new( 34 | :comment => 'comment', :comment_sep => ';', :comment_offset => 15 35 | ).to_ini).to eq('fake line ; comment') 36 | end 37 | 38 | it 'should use ";" as a default comment seperator' do 39 | expect(IniParse::Test::FakeLine.new( 40 | :comment => 'comment' 41 | ).to_ini).to eq('fake line ; comment') 42 | end 43 | 44 | it 'should use the correct seperator' do 45 | expect(IniParse::Test::FakeLine.new( 46 | :comment => 'comment', :comment_sep => '#' 47 | ).to_ini).to eq('fake line # comment') 48 | end 49 | 50 | it 'should use the ensure a space is added before the comment seperator' do 51 | expect(IniParse::Test::FakeLine.new( 52 | :comment => 'comment', :comment_sep => ';', :comment_offset => 0 53 | ).to_ini).to eq('fake line ; comment') 54 | end 55 | 56 | it 'should not add an extra space if the line is blank' do 57 | line = IniParse::Test::FakeLine.new( 58 | :comment => 'comment', :comment_sep => ';', :comment_offset => 0 59 | ) 60 | 61 | allow(line).to receive(:line_contents).and_return('') 62 | expect(line.to_ini).to eq('; comment') 63 | end 64 | end 65 | 66 | describe 'when no comment is set' do 67 | it 'should not add trailing space if :comment_offset has a value' do 68 | expect(IniParse::Test::FakeLine.new(:comment_offset => 10).to_ini).to eq('fake line') 69 | end 70 | 71 | it 'should not add a comment seperator :comment_sep has a value' do 72 | expect(IniParse::Test::FakeLine.new(:comment_sep => ';').to_ini).to eq('fake line') 73 | end 74 | end 75 | end 76 | 77 | describe '#has_comment?' do 78 | it 'should return true if :comment has a non-blank value' do 79 | expect(IniParse::Test::FakeLine.new(:comment => 'comment')).to have_comment 80 | end 81 | 82 | it 'should return true if :comment has a blank value' do 83 | expect(IniParse::Test::FakeLine.new(:comment => '')).to have_comment 84 | end 85 | 86 | it 'should return false if :comment has a nil value' do 87 | expect(IniParse::Test::FakeLine.new).not_to have_comment 88 | expect(IniParse::Test::FakeLine.new(:comment => nil)).not_to have_comment 89 | end 90 | end 91 | end 92 | 93 | # 94 | # Section 95 | # 96 | 97 | describe 'IniParse::Lines::Section' do 98 | before(:each) { @section = IniParse::Lines::Section.new('a section') } 99 | 100 | it 'should respond_to +lines+' do 101 | expect(@section).to respond_to(:lines) 102 | end 103 | 104 | it 'should not respond_to +lines=+' do 105 | expect(@section).not_to respond_to(:lines=) 106 | end 107 | 108 | it 'should include Enumerable' do 109 | expect(IniParse::Lines::Section.included_modules).to include(Enumerable) 110 | end 111 | 112 | describe '#initialize' do 113 | it 'should typecast the given key to a string' do 114 | expect(IniParse::Lines::Section.new(:symbol).key).to eq('symbol') 115 | end 116 | end 117 | 118 | describe '#option' do 119 | it 'should retrieve the line identified by the given key' do 120 | option = IniParse::Lines::Option.new('k', 'value one') 121 | @section.lines << option 122 | expect(@section.option('k')).to eq(option) 123 | end 124 | 125 | it 'should return nil if the given key does not exist' do 126 | expect(@section.option('does_not_exist')).to be_nil 127 | end 128 | end 129 | 130 | describe '#each' do 131 | it 'should call #each on +lines+' do 132 | expect(@section.lines).to receive(:each) 133 | @section.each { |l| } 134 | end 135 | end 136 | 137 | describe '#[]' do 138 | it 'should return nil if the given key does not exist' do 139 | expect(@section['k']).to be_nil 140 | end 141 | 142 | it 'should return a value if the given key exists' do 143 | @section.lines << IniParse::Lines::Option.new('k', 'v') 144 | expect(@section['k']).to eq('v') 145 | end 146 | 147 | it 'should return an array of values if the key is a duplicate' do 148 | @section.lines << IniParse::Lines::Option.new('k', 'v1') 149 | @section.lines << IniParse::Lines::Option.new('k', 'v2') 150 | @section.lines << IniParse::Lines::Option.new('k', 'v3') 151 | expect(@section['k']).to eq(['v1', 'v2', 'v3']) 152 | end 153 | 154 | it 'should typecast the key to a string' do 155 | @section.lines << IniParse::Lines::Option.new('k', 'v') 156 | expect(@section[:k]).to eq('v') 157 | end 158 | end 159 | 160 | describe '#[]=' do 161 | it 'should add a new Option with the given key and value' do 162 | @section['k'] = 'a value' 163 | expect(@section.option('k')).to be_kind_of(IniParse::Lines::Option) 164 | expect(@section['k']).to eq('a value') 165 | end 166 | 167 | it 'should update the Option if one already exists' do 168 | @section.lines << IniParse::Lines::Option.new('k', 'orig value') 169 | @section['k'] = 'new value' 170 | expect(@section['k']).to eq('new value') 171 | end 172 | 173 | it 'should replace the existing Option if it is an array' do 174 | @section.lines << IniParse::Lines::Option.new('k', 'v1') 175 | @section.lines << IniParse::Lines::Option.new('k', 'v2') 176 | @section['k'] = 'new value' 177 | expect(@section.option('k')).to be_kind_of(IniParse::Lines::Option) 178 | expect(@section['k']).to eq('new value') 179 | end 180 | 181 | it 'should typecast the key to a string' do 182 | @section[:k] = 'a value' 183 | expect(@section['k']).to eq('a value') 184 | end 185 | end 186 | 187 | describe '#merge!' do 188 | before(:each) do 189 | @section.lines << IniParse::Lines::Option.new('a', 'val1') 190 | @section.lines << IniParse::Lines::Blank.new 191 | @section.lines << IniParse::Lines::Comment.new 192 | @section.lines << IniParse::Lines::Option.new('b', 'val2') 193 | 194 | @new_section = IniParse::Lines::Section.new('new section') 195 | end 196 | 197 | it 'should merge options from the given Section into the receiver' do 198 | @new_section.lines << IniParse::Lines::Option.new('c', 'val3') 199 | @new_section.lines << IniParse::Lines::Option.new('d', 'val4') 200 | 201 | @section.merge!(@new_section) 202 | expect(@section['a']).to eq('val1') 203 | expect(@section['b']).to eq('val2') 204 | expect(@section['c']).to eq('val3') 205 | expect(@section['d']).to eq('val4') 206 | end 207 | 208 | it 'should handle duplicates' do 209 | @new_section.lines << IniParse::Lines::Option.new('a', 'val2') 210 | @section.merge!(@new_section) 211 | expect(@section['a']).to eq(['val1', 'val2']) 212 | end 213 | 214 | it 'should handle duplicates on both sides' do 215 | @section.lines << IniParse::Lines::Option.new('a', 'val2') 216 | @new_section.lines << IniParse::Lines::Option.new('a', 'val3') 217 | @new_section.lines << IniParse::Lines::Option.new('a', 'val4') 218 | 219 | @section.merge!(@new_section) 220 | expect(@section['a']).to eq(['val1', 'val2', 'val3', 'val4']) 221 | end 222 | 223 | it 'should copy blank lines' do 224 | @new_section.lines << IniParse::Lines::Blank.new 225 | @section.merge!(@new_section) 226 | line = nil 227 | @section.each(true) { |l| line = l } 228 | expect(line).to be_kind_of(IniParse::Lines::Blank) 229 | end 230 | 231 | it 'should copy comments' do 232 | @new_section.lines << IniParse::Lines::Comment.new 233 | @section.merge!(@new_section) 234 | line = nil 235 | @section.each(true) { |l| line = l } 236 | expect(line).to be_kind_of(IniParse::Lines::Comment) 237 | end 238 | end 239 | 240 | describe '#delete' do 241 | let(:opt_one) { IniParse::Lines::Option.new('a', 'b') } 242 | let(:opt_two) { IniParse::Lines::Option.new('c', 'd') } 243 | 244 | before(:each) do 245 | @section.lines << opt_one 246 | @section.lines << opt_two 247 | end 248 | 249 | it 'removes the option given a key' do 250 | expect { @section.delete('a') }. 251 | to change { @section['a'] }.to(nil) 252 | end 253 | 254 | it 'removes the option given an Option' do 255 | expect { @section.delete(opt_one) }. 256 | to change { @section['a'] }.to(nil) 257 | end 258 | 259 | it 'should not remove non-matching lines' do 260 | expect { @section.delete('a') }.not_to change { @section['c'] } 261 | end 262 | 263 | it 'returns the section' do 264 | expect(@section.delete('a')).to eql(@section) 265 | end 266 | end 267 | 268 | describe '#to_ini' do 269 | it 'should include the section key' do 270 | expect(IniParse::Lines::Section.new('a section').to_ini).to eq('[a section]') 271 | end 272 | 273 | it 'should include lines belonging to the section' do 274 | @section.lines << IniParse::Lines::Option.new('a', 'val1') 275 | @section.lines << IniParse::Lines::Blank.new 276 | @section.lines << IniParse::Lines::Comment.new( 277 | :comment => 'my comment', :comment_sep => ';', :comment_offset => 0 278 | ) 279 | @section.lines << IniParse::Lines::Option.new('b', 'val2') 280 | 281 | expect(@section.to_ini).to eq( 282 | "[a section]\n" \ 283 | "a = val1\n" \ 284 | "\n" \ 285 | "; my comment\n" \ 286 | "b = val2" 287 | ) 288 | end 289 | 290 | it 'should include duplicate lines' do 291 | @section.lines << IniParse::Lines::Option.new('a', 'val1') 292 | @section.lines << IniParse::Lines::Option.new('a', 'val2') 293 | 294 | expect(@section.to_ini).to eq( 295 | "[a section]\n" \ 296 | "a = val1\n" \ 297 | "a = val2" 298 | ) 299 | end 300 | end 301 | 302 | describe '#has_option?' do 303 | before do 304 | @section['first'] = 'value' 305 | end 306 | 307 | it 'should return true if an option with the given key exists' do 308 | expect(@section).to have_option('first') 309 | end 310 | 311 | it 'should return true if no option with the given key exists' do 312 | expect(@section).not_to have_option('second') 313 | end 314 | end 315 | end 316 | 317 | # 318 | # Option 319 | # 320 | 321 | describe 'Iniparse::Lines::Option' do 322 | describe '#initialize' do 323 | it 'should typecast the given key to a string' do 324 | expect(IniParse::Lines::Option.new(:symbol, '').key).to eq('symbol') 325 | end 326 | end 327 | 328 | describe '#to_ini' do 329 | it 'should include the key and value' do 330 | expect(IniParse::Lines::Option.new('key', 'value').to_ini).to eq('key = value') 331 | end 332 | end 333 | 334 | describe '.parse' do 335 | def parse(line, opts = {}) 336 | IniParse::Lines::Option.parse(line, opts) 337 | end 338 | 339 | it 'should typecast empty values to nil' do 340 | expect(parse('key =')).to be_option_tuple('key', nil) 341 | expect(parse('key = ')).to be_option_tuple('key', nil) 342 | expect(parse('key = ')).to be_option_tuple('key', nil) 343 | end 344 | 345 | it 'should not typecast "true" if true is part of a word' do 346 | expect(parse('key = TestTrueTest')).to be_option_tuple('key', 'TestTrueTest') 347 | expect(parse('key = TrueTest')).to be_option_tuple('key', 'TrueTest') 348 | expect(parse('key = TestTrue')).to be_option_tuple('key', 'TestTrue') 349 | end 350 | 351 | it 'should not typecast "false" if false is part of a word' do 352 | expect(parse('key = TestFalseTest')).to be_option_tuple('key', 'TestFalseTest') 353 | expect(parse('key = FalseTest')).to be_option_tuple('key', 'FalseTest') 354 | expect(parse('key = TestFalse')).to be_option_tuple('key', 'TestFalse') 355 | end 356 | 357 | it 'should typecast "true" to TrueClass' do 358 | expect(parse('key = true')).to be_option_tuple('key', true) 359 | expect(parse('key = TRUE')).to be_option_tuple('key', true) 360 | end 361 | 362 | it 'should typecast "false" to FalseClass' do 363 | expect(parse('key = false')).to be_option_tuple('key', false) 364 | expect(parse('key = FALSE')).to be_option_tuple('key', false) 365 | end 366 | 367 | it 'should typecast integer values to Integer' do 368 | expect(parse('key = 1')).to be_option_tuple('key', 1) 369 | expect(parse('key = 10')).to be_option_tuple('key', 10) 370 | end 371 | 372 | it 'should not typecast integers with a leading 0 to Integer' do 373 | expect(parse('key = 0700')).to be_option_tuple('key', '0700') 374 | end 375 | 376 | it 'should typecast negative integer values to Integer' do 377 | expect(parse('key = -1')).to be_option_tuple('key', -1) 378 | end 379 | 380 | it 'should typecast float values to Float' do 381 | expect(parse('key = 3.14159265')).to be_option_tuple('key', 3.14159265) 382 | end 383 | 384 | it 'should typecast negative float values to Float' do 385 | expect(parse('key = -3.14159265')).to be_option_tuple('key', -3.14159265) 386 | end 387 | 388 | it 'should typecast scientific notation numbers to Float' do 389 | expect(parse('key = 10e5')).to be_option_tuple('key', 10e5) 390 | expect(parse('key = 10e+5')).to be_option_tuple('key', 10e5) 391 | expect(parse('key = 10e-5')).to be_option_tuple('key', 10e-5) 392 | 393 | expect(parse('key = -10e5')).to be_option_tuple('key', -10e5) 394 | expect(parse('key = -10e+5')).to be_option_tuple('key', -10e5) 395 | expect(parse('key = -10e-5')).to be_option_tuple('key', -10e-5) 396 | 397 | expect(parse('key = 3.14159265e5')).to be_option_tuple('key', 3.14159265e5) 398 | expect(parse('key = 3.14159265e+5')).to be_option_tuple('key', 3.14159265e5) 399 | expect(parse('key = 3.14159265e-5')).to be_option_tuple('key', 3.14159265e-5) 400 | 401 | expect(parse('key = -3.14159265e5')).to be_option_tuple('key', -3.14159265e5) 402 | expect(parse('key = -3.14159265e+5')).to be_option_tuple('key', -3.14159265e5) 403 | expect(parse('key = -3.14159265e-5')).to be_option_tuple('key', -3.14159265e-5) 404 | end 405 | end 406 | end 407 | 408 | # 409 | # Blank 410 | # 411 | 412 | # 413 | # Comment 414 | # 415 | 416 | describe 'IniParse::Lines::Comment' do 417 | describe '#has_comment?' do 418 | it 'should return true if :comment has a non-blank value' do 419 | expect(IniParse::Lines::Comment.new(:comment => 'comment')).to have_comment 420 | end 421 | 422 | it 'should return true if :comment has a blank value' do 423 | expect(IniParse::Lines::Comment.new(:comment => '')).to have_comment 424 | end 425 | 426 | it 'should return true if :comment has a nil value' do 427 | expect(IniParse::Lines::Comment.new).to have_comment 428 | expect(IniParse::Lines::Comment.new(:comment => nil)).to have_comment 429 | end 430 | end 431 | 432 | describe '#to_ini' do 433 | it 'should return the comment' do 434 | expect(IniParse::Lines::Comment.new( 435 | :comment => 'a comment' 436 | ).to_ini).to eq('; a comment') 437 | end 438 | 439 | it 'should preserve comment offset' do 440 | expect(IniParse::Lines::Comment.new( 441 | :comment => 'a comment', :comment_offset => 10 442 | ).to_ini).to eq(' ; a comment') 443 | end 444 | 445 | it 'should return just the comment_sep if the comment is blank' do 446 | expect(IniParse::Lines::Comment.new.to_ini).to eq(';') 447 | end 448 | end 449 | end 450 | -------------------------------------------------------------------------------- /spec/parser/document_parsing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests parsing of multiple lines, in context, using #parse. 4 | 5 | describe 'Parsing a document' do 6 | describe 'when a comment preceeds a single section and option' do 7 | before(:all) do 8 | @doc = IniParse::Parser.new(fixture(:comment_before_section)).parse 9 | end 10 | 11 | it 'should have a comment as the first line' do 12 | expect(@doc.lines.to_a.first).to be_kind_of(IniParse::Lines::Comment) 13 | end 14 | 15 | it 'should have one section' do 16 | expect(@doc.lines.keys).to eq(['first_section']) 17 | end 18 | 19 | it 'should have one option belonging to `first_section`' do 20 | expect(@doc['first_section']['key']).to eq('value') 21 | end 22 | end 23 | 24 | it 'should allow blank lines to preceed the first section' do 25 | expect { 26 | @doc = IniParse::Parser.new(fixture(:blank_before_section)).parse 27 | }.not_to raise_error 28 | 29 | expect(@doc.lines.to_a.first).to be_kind_of(IniParse::Lines::Blank) 30 | end 31 | 32 | it 'should allow a blank line to belong to a section' do 33 | expect { 34 | @doc = IniParse::Parser.new(fixture(:blank_in_section)).parse 35 | }.not_to raise_error 36 | 37 | expect(@doc['first_section'].lines.to_a.first).to be_kind_of(IniParse::Lines::Blank) 38 | end 39 | 40 | it 'should permit comments on their own line' do 41 | expect { 42 | @doc = IniParse::Parser.new(fixture(:comment_line)).parse 43 | }.not_to raise_error 44 | 45 | line = @doc['first_section'].lines.to_a.first 46 | expect(line.comment).to eql('; block comment ;') 47 | end 48 | 49 | it 'should permit options before the first section' do 50 | doc = IniParse::Parser.new(fixture(:option_before_section)).parse 51 | 52 | expect(doc.lines).to have_key('__anonymous__') 53 | expect(doc['__anonymous__']['foo']).to eql('bar') 54 | expect(doc['foo']['another']).to eql('thing') 55 | end 56 | 57 | it 'should raise ParseError if a line could not be parsed' do 58 | expect { IniParse::Parser.new(fixture(:invalid_line)).parse }.to \ 59 | raise_error(IniParse::ParseError) 60 | end 61 | 62 | describe 'when a section name contains "="' do 63 | before(:all) do 64 | @doc = IniParse::Parser.new(fixture(:section_with_equals)).parse 65 | end 66 | 67 | it 'should have two sections' do 68 | expect(@doc.lines.to_a.length).to eq(2) 69 | end 70 | 71 | it 'should have one section' do 72 | expect(@doc.lines.keys).to eq(['first_section = name', 73 | 'another_section = a name']) 74 | end 75 | 76 | it 'should have one option belonging to `first_section = name`' do 77 | expect(@doc['first_section = name']['key']).to eq('value') 78 | end 79 | 80 | it 'should have one option belonging to `another_section = a name`' do 81 | expect(@doc['another_section = a name']['another']).to eq('thing') 82 | end 83 | end 84 | 85 | describe 'when a document contains a section multiple times' do 86 | before(:all) do 87 | @doc = IniParse::Parser.new(fixture(:duplicate_section)).parse 88 | end 89 | 90 | it 'should only add the section once' do 91 | # "first_section" and "second_section". 92 | expect(@doc.lines.to_a.length).to eq(2) 93 | end 94 | 95 | it 'should retain values from the first time' do 96 | expect(@doc['first_section']['key']).to eq('value') 97 | end 98 | 99 | it 'should add new keys' do 100 | expect(@doc['first_section']['third']).to eq('fourth') 101 | end 102 | 103 | it 'should merge in duplicate keys' do 104 | expect(@doc['first_section']['another']).to eq(%w( thing again )) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/parser/line_parsing_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # Tests parsing of individual, out of context, line types using #parse_line. 4 | 5 | describe 'Parsing a line' do 6 | it 'should strip leading whitespace and set the :indent option' do 7 | expect(IniParse::Parser.parse_line(' [section]')).to \ 8 | be_section_tuple(:any, {:indent => ' '}) 9 | end 10 | 11 | it 'should raise an error if the line could not be matched' do 12 | expect { IniParse::Parser.parse_line('invalid line') }.to \ 13 | raise_error(IniParse::ParseError) 14 | end 15 | 16 | it 'should parse using the types set in IniParse::Parser.parse_types' do 17 | begin 18 | # Remove last type. 19 | type = IniParse::Parser.parse_types.pop 20 | expect(type).not_to receive(:parse) 21 | IniParse::Parser.parse_line('[section]') 22 | ensure 23 | IniParse::Parser.parse_types << type 24 | end 25 | end 26 | 27 | # -- 28 | # ========================================================================== 29 | # Option lines. 30 | # ========================================================================== 31 | # ++ 32 | 33 | describe 'with "k = v"' do 34 | before(:all) do 35 | @tuple = IniParse::Parser.parse_line('k = v') 36 | end 37 | 38 | it 'should return an option tuple' do 39 | expect(@tuple).to be_option_tuple('k', 'v') 40 | end 41 | 42 | it 'should set no indent, comment, offset or separator' do 43 | expect(@tuple.last[:indent]).to be_nil 44 | expect(@tuple.last[:comment]).to be_nil 45 | expect(@tuple.last[:comment_offset]).to be_nil 46 | expect(@tuple.last[:comment_sep]).to be_nil 47 | end 48 | end 49 | 50 | describe 'with "k = a value with spaces"' do 51 | it 'should return an option tuple' do 52 | expect(IniParse::Parser.parse_line('k = a value with spaces')).to \ 53 | be_option_tuple('k', 'a value with spaces') 54 | end 55 | end 56 | 57 | describe 'with "k = v ; a comment "' do 58 | before(:all) do 59 | @tuple = IniParse::Parser.parse_line('k = v ; a comment') 60 | end 61 | 62 | it 'should return an option tuple' do 63 | expect(@tuple).to be_option_tuple('k', 'v') 64 | end 65 | 66 | it 'should set the comment to "a comment"' do 67 | expect(@tuple).to be_option_tuple(:any, :any, :comment => 'a comment') 68 | end 69 | 70 | it 'should set the comment separator to ";"' do 71 | expect(@tuple).to be_option_tuple(:any, :any, :comment_sep => ';') 72 | end 73 | 74 | it 'should set the comment offset to 6' do 75 | expect(@tuple).to be_option_tuple(:any, :any, :comment_offset => 6) 76 | end 77 | end 78 | 79 | describe 'with "k = v;w;x y;z"' do 80 | before(:all) do 81 | @tuple = IniParse::Parser.parse_line('k = v;w;x y;z') 82 | end 83 | 84 | it 'should return an option tuple with the correct value' do 85 | expect(@tuple).to be_option_tuple(:any, 'v;w;x y;z') 86 | end 87 | 88 | it 'should not set a comment' do 89 | expect(@tuple.last[:comment]).to be_nil 90 | expect(@tuple.last[:comment_offset]).to be_nil 91 | expect(@tuple.last[:comment_sep]).to be_nil 92 | end 93 | end 94 | 95 | describe 'with "k = v;w ; a comment"' do 96 | before(:all) do 97 | @tuple = IniParse::Parser.parse_line('k = v;w ; a comment') 98 | end 99 | 100 | it 'should return an option tuple with the correct value' do 101 | expect(@tuple).to be_option_tuple(:any, 'v;w') 102 | end 103 | 104 | it 'should set the comment to "a comment"' do 105 | expect(@tuple).to be_option_tuple(:any, :any, :comment => 'a comment') 106 | end 107 | 108 | it 'should set the comment separator to ";"' do 109 | expect(@tuple).to be_option_tuple(:any, :any, :comment_sep => ';') 110 | end 111 | 112 | it 'should set the comment offset to 8' do 113 | expect(@tuple).to be_option_tuple(:any, :any, :comment_offset => 8) 114 | end 115 | end 116 | 117 | describe 'with "key=value"' do 118 | it 'should return an option tuple with the correct key and value' do 119 | expect(IniParse::Parser.parse_line('key=value')).to \ 120 | be_option_tuple('key', 'value') 121 | end 122 | end 123 | 124 | describe 'with "key= value"' do 125 | it 'should return an option tuple with the correct key and value' do 126 | expect(IniParse::Parser.parse_line('key= value')).to \ 127 | be_option_tuple('key', 'value') 128 | end 129 | end 130 | 131 | describe 'with "key =value"' do 132 | it 'should return an option tuple with the correct key and value' do 133 | expect(IniParse::Parser.parse_line('key =value')).to \ 134 | be_option_tuple('key', 'value') 135 | end 136 | end 137 | 138 | describe 'with "key = value"' do 139 | it 'should return an option tuple with the correct key and value' do 140 | expect(IniParse::Parser.parse_line('key = value')).to \ 141 | be_option_tuple('key', 'value') 142 | end 143 | end 144 | 145 | describe 'with "key ="' do 146 | it 'should return an option tuple with the correct key' do 147 | expect(IniParse::Parser.parse_line('key =')).to \ 148 | be_option_tuple('key') 149 | end 150 | 151 | it 'should set the option value to nil' do 152 | expect(IniParse::Parser.parse_line('key =')).to \ 153 | be_option_tuple(:any, nil) 154 | end 155 | end 156 | 157 | 158 | describe 'with "key = EEjDDJJjDJDJD233232=="' do 159 | it 'should include the "equals" in the option value' do 160 | expect(IniParse::Parser.parse_line('key = EEjDDJJjDJDJD233232==')).to \ 161 | be_option_tuple('key', 'EEjDDJJjDJDJD233232==') 162 | end 163 | end 164 | 165 | describe 'with "key = ==EEjDDJJjDJDJD233232"' do 166 | it 'should include the "equals" in the option value' do 167 | expect(IniParse::Parser.parse_line('key = ==EEjDDJJjDJDJD233232')).to \ 168 | be_option_tuple('key', '==EEjDDJJjDJDJD233232') 169 | end 170 | end 171 | 172 | describe 'with "key.two = value"' do 173 | it 'should return an option tuple with the correct key' do 174 | expect(IniParse::Parser.parse_line('key.two = value')).to \ 175 | be_option_tuple('key.two') 176 | end 177 | end 178 | 179 | describe 'with "key/with/slashes = value"' do 180 | it 'should return an option tuple with the correct key' do 181 | expect(IniParse::Parser.parse_line('key/with/slashes = value')).to \ 182 | be_option_tuple('key/with/slashes', 'value') 183 | end 184 | end 185 | 186 | describe 'with "key_with_underscores = value"' do 187 | it 'should return an option tuple with the correct key' do 188 | expect(IniParse::Parser.parse_line('key_with_underscores = value')).to \ 189 | be_option_tuple('key_with_underscores', 'value') 190 | end 191 | end 192 | 193 | describe 'with "key-with-dashes = value"' do 194 | it 'should return an option tuple with the correct key' do 195 | expect(IniParse::Parser.parse_line('key-with-dashes = value')).to \ 196 | be_option_tuple('key-with-dashes', 'value') 197 | end 198 | end 199 | 200 | describe 'with "key with spaces = value"' do 201 | it 'should return an option tuple with the correct key' do 202 | expect(IniParse::Parser.parse_line('key with spaces = value')).to \ 203 | be_option_tuple('key with spaces', 'value') 204 | end 205 | end 206 | 207 | # -- 208 | # ========================================================================== 209 | # Section lines. 210 | # ========================================================================== 211 | # ++ 212 | 213 | describe 'with "[section]"' do 214 | before(:all) do 215 | @tuple = IniParse::Parser.parse_line('[section]') 216 | end 217 | 218 | it 'should return a section tuple' do 219 | expect(@tuple).to be_section_tuple('section') 220 | end 221 | 222 | it 'should set no indent, comment, offset or separator' do 223 | expect(@tuple.last[:indent]).to be_nil 224 | expect(@tuple.last[:comment]).to be_nil 225 | expect(@tuple.last[:comment_offset]).to be_nil 226 | expect(@tuple.last[:comment_sep]).to be_nil 227 | end 228 | end 229 | 230 | describe 'with "[section with whitespace]"' do 231 | it 'should return a section tuple with the correct key' do 232 | expect(IniParse::Parser.parse_line('[section with whitespace]')).to \ 233 | be_section_tuple('section with whitespace') 234 | end 235 | end 236 | 237 | describe 'with "[ section with surounding whitespace ]"' do 238 | it 'should return a section tuple with the correct key' do 239 | expect(IniParse::Parser.parse_line('[ section with surounding whitespace ]')).to \ 240 | be_section_tuple(' section with surounding whitespace ') 241 | end 242 | end 243 | 244 | describe 'with "[section] ; a comment"' do 245 | before(:all) do 246 | @tuple = IniParse::Parser.parse_line('[section] ; a comment') 247 | end 248 | 249 | it 'should return a section tuple' do 250 | expect(@tuple).to be_section_tuple('section') 251 | end 252 | 253 | it 'should set the comment to "a comment"' do 254 | expect(@tuple).to be_section_tuple(:any, :comment => 'a comment') 255 | end 256 | 257 | it 'should set the comment separator to ";"' do 258 | expect(@tuple).to be_section_tuple(:any, :comment_sep => ';') 259 | end 260 | 261 | it 'should set the comment offset to 10' do 262 | expect(@tuple).to be_section_tuple(:any, :comment_offset => 10) 263 | end 264 | end 265 | 266 | describe 'with "[section;with#comment;chars]"' do 267 | before(:all) do 268 | @tuple = IniParse::Parser.parse_line('[section;with#comment;chars]') 269 | end 270 | 271 | it 'should return a section tuple with the correct key' do 272 | expect(@tuple).to be_section_tuple('section;with#comment;chars') 273 | end 274 | 275 | it 'should not set a comment' do 276 | expect(@tuple.last[:indent]).to be_nil 277 | expect(@tuple.last[:comment]).to be_nil 278 | expect(@tuple.last[:comment_offset]).to be_nil 279 | expect(@tuple.last[:comment_sep]).to be_nil 280 | end 281 | end 282 | 283 | describe 'with "[section;with#comment;chars] ; a comment"' do 284 | before(:all) do 285 | @tuple = IniParse::Parser.parse_line('[section;with#comment;chars] ; a comment') 286 | end 287 | 288 | it 'should return a section tuple with the correct key' do 289 | expect(@tuple).to be_section_tuple('section;with#comment;chars') 290 | end 291 | 292 | it 'should set the comment to "a comment"' do 293 | expect(@tuple).to be_section_tuple(:any, :comment => 'a comment') 294 | end 295 | 296 | it 'should set the comment separator to ";"' do 297 | expect(@tuple).to be_section_tuple(:any, :comment_sep => ';') 298 | end 299 | 300 | it 'should set the comment offset to 29' do 301 | expect(@tuple).to be_section_tuple(:any, :comment_offset => 29) 302 | end 303 | end 304 | 305 | # -- 306 | # ========================================================================== 307 | # Comment lines. 308 | # ========================================================================== 309 | # ++ 310 | 311 | describe 'with "; a comment"' do 312 | before(:all) do 313 | @tuple = IniParse::Parser.parse_line('; a comment') 314 | end 315 | 316 | it 'should return a comment tuple with the correct comment' do 317 | expect(@tuple).to be_comment_tuple('a comment') 318 | end 319 | 320 | it 'should set the comment separator to ";"' do 321 | expect(@tuple).to be_comment_tuple(:any, :comment_sep => ';') 322 | end 323 | 324 | it 'should set the comment offset to 0' do 325 | expect(@tuple).to be_comment_tuple(:any, :comment_offset => 0) 326 | end 327 | end 328 | 329 | describe 'with " ; a comment"' do 330 | before(:all) do 331 | @tuple = IniParse::Parser.parse_line(' ; a comment') 332 | end 333 | 334 | it 'should return a comment tuple with the correct comment' do 335 | expect(@tuple).to be_comment_tuple('a comment') 336 | end 337 | 338 | it 'should set the comment separator to ";"' do 339 | expect(@tuple).to be_comment_tuple(:any, :comment_sep => ';') 340 | end 341 | 342 | it 'should set the comment offset to 1' do 343 | expect(@tuple).to be_comment_tuple(:any, :comment_offset => 1) 344 | end 345 | end 346 | 347 | describe 'with ";"' do 348 | before(:all) do 349 | @tuple = IniParse::Parser.parse_line(';') 350 | end 351 | 352 | it 'should return a comment tuple with an empty value' do 353 | expect(@tuple).to be_comment_tuple('') 354 | end 355 | 356 | it 'should set the comment separator to ";"' do 357 | expect(@tuple).to be_comment_tuple(:any, :comment_sep => ';') 358 | end 359 | 360 | it 'should set the comment offset to 0' do 361 | expect(@tuple).to be_comment_tuple(:any, :comment_offset => 0) 362 | end 363 | end 364 | 365 | # -- 366 | # ========================================================================== 367 | # Blank lines. 368 | # ========================================================================== 369 | # ++ 370 | 371 | describe 'with ""' do 372 | it 'should return a blank tuple' do 373 | expect(IniParse::Parser.parse_line('')).to be_blank_tuple 374 | end 375 | end 376 | 377 | describe 'with " "' do 378 | it 'should return a blank tuple' do 379 | expect(IniParse::Parser.parse_line(' ')).to be_blank_tuple 380 | end 381 | end 382 | end 383 | -------------------------------------------------------------------------------- /spec/spec_fixtures.rb: -------------------------------------------------------------------------------- 1 | module IniParse 2 | module Test 3 | class Fixtures 4 | @@fixtures = {} 5 | 6 | def self.[](fix) 7 | if @@fixtures.has_key?(fix) 8 | @@fixtures[fix] 9 | else 10 | @@fixtures[fix] = File.read( 11 | File.join(File.expand_path('fixtures', File.dirname(__FILE__)), fix) 12 | ) 13 | end 14 | end 15 | 16 | def self.[]=(fix, val) 17 | @@fixtures[fix] = val 18 | end 19 | end 20 | end 21 | end 22 | 23 | IniParse::Test::Fixtures[:comment_before_section] = <<-FIX.gsub(/^ /, '') 24 | ; This is a comment 25 | [first_section] 26 | key = value 27 | FIX 28 | 29 | IniParse::Test::Fixtures[:blank_before_section] = <<-FIX.gsub(/^ /, '') 30 | 31 | [first_section] 32 | key = value 33 | FIX 34 | 35 | IniParse::Test::Fixtures[:blank_in_section] = <<-FIX.gsub(/^ /, '') 36 | [first_section] 37 | 38 | key = value 39 | FIX 40 | 41 | IniParse::Test::Fixtures[:option_before_section] = <<-FIX.gsub(/^ /, '') 42 | foo = bar 43 | [foo] 44 | another = thing 45 | FIX 46 | 47 | IniParse::Test::Fixtures[:invalid_line] = <<-FIX.gsub(/^ /, '') 48 | this line is not valid 49 | FIX 50 | 51 | IniParse::Test::Fixtures[:section_with_equals] = <<-FIX.gsub(/^ /, '') 52 | [first_section = name] 53 | key = value 54 | [another_section = a name] 55 | another = thing 56 | FIX 57 | 58 | IniParse::Test::Fixtures[:comment_line] = <<-FIX.gsub(/^ /, '') 59 | [first_section] 60 | ; block comment ; 61 | ; with more lines ; 62 | key = value 63 | FIX 64 | 65 | IniParse::Test::Fixtures[:duplicate_section] = <<-FIX.gsub(/^ /, '') 66 | [first_section] 67 | key = value 68 | another = thing 69 | 70 | [second_section] 71 | okay = yes 72 | 73 | [first_section] 74 | third = fourth 75 | another = again 76 | FIX 77 | 78 | IniParse::Test::Fixtures[:dos_endings] = 79 | "[database]\r\n" \ 80 | "first = true\r\n" \ 81 | "second = false\r\n" 82 | 83 | # https://github.com/antw/iniparse/issues/17 84 | IniParse::Test::Fixtures[:anon_section_with_comments] = <<-FIX.gsub(/^ /, '') 85 | ##################### 86 | # A lot of comments # 87 | ##################### 88 | 89 | # optiona comment 90 | optiona = A 91 | 92 | # optionb comment 93 | optionb = B 94 | 95 | # optionc comment 96 | optionc = C 97 | FIX 98 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.push File.join(File.dirname(__FILE__), '..', 'lib') 2 | 3 | require 'rubygems' 4 | require 'rspec' 5 | 6 | require 'iniparse' 7 | require File.join(File.dirname(__FILE__), 'spec_fixtures') 8 | 9 | module IniParse 10 | module Test 11 | module Helpers 12 | # Taken from Merb Core's spec helper. 13 | # Merb is licenced using the MIT License and is copyright 14 | # Engine Yard Inc. 15 | class BeKindOf 16 | def initialize(expected) # + args 17 | @expected = expected 18 | end 19 | 20 | def matches?(target) 21 | @target = target 22 | @target.kind_of?(@expected) 23 | end 24 | 25 | def failure_message 26 | "expected #{@expected} but got #{@target.class}" 27 | end 28 | 29 | def failure_message_when_negated 30 | "expected #{@expected} to not be #{@target.class}" 31 | end 32 | 33 | def description 34 | "be_kind_of #{@target}" 35 | end 36 | end 37 | 38 | # Used to match line tuples returned by Parser.parse_line. 39 | class BeLineTuple 40 | def initialize(type, value_keys = [], *expected) 41 | @expected_type = type 42 | @value_keys = value_keys 43 | 44 | if expected.nil? 45 | @expected_opts = {} 46 | @expected_values = [] 47 | else 48 | @expected_opts = expected.pop 49 | @expected_values = expected 50 | end 51 | end 52 | 53 | def matches?(tuple) 54 | @tuple = tuple 55 | 56 | @failure_message = catch(:fail) do 57 | tuple? 58 | correct_type? 59 | correct_values? 60 | correct_opts? 61 | true 62 | end 63 | 64 | @failure_message == true 65 | end 66 | 67 | def failure_message 68 | "expected #{@expected_type} tuple #{@failure_message}" 69 | end 70 | 71 | def failure_message_when_negated 72 | "expected #{@tuple.inspect} to not be #{@expected_type} tuple" 73 | end 74 | 75 | def description 76 | "be_#{@expected_type}_tuple #{@tuple}" 77 | end 78 | 79 | ####### 80 | private 81 | ####### 82 | 83 | # Matchers. 84 | 85 | def tuple? 86 | throw :fail, "but was #{@tuple.class}" unless @tuple.kind_of?(Array) 87 | end 88 | 89 | def correct_type? 90 | throw :fail, "but was #{type} tuple" unless type == @expected_type 91 | end 92 | 93 | def correct_values? 94 | # Make sure the values match. 95 | @value_keys.each_with_index do |key, i| 96 | if @expected_values[i] != :any && values[i] != @expected_values[i] 97 | throw :fail, 'with %s value of "%s" but was "%s"' % [ 98 | key, values[i], @expected_values[i] 99 | ] 100 | end 101 | end 102 | end 103 | 104 | def correct_opts? 105 | if(! @expected_opts.nil?) 106 | if (! @expected_opts.empty?) && opts.empty? 107 | throw :fail, 'with options but there were none' 108 | end 109 | 110 | @expected_opts.each do |key, value| 111 | unless opts.has_key?(key) 112 | throw :fail, 'with option "%s", but key was not present' % key 113 | end 114 | 115 | unless opts[key] == value 116 | throw :fail, 'with option "%s" => "%s" but was "%s"' % [ 117 | key, value, opts[key] 118 | ] 119 | end 120 | end 121 | end 122 | end 123 | 124 | # Tuple values, etc. 125 | 126 | def type 127 | @type ||= @tuple.first 128 | end 129 | 130 | def values 131 | @values ||= @tuple.length < 3 ? [] : @tuple[1..-2] 132 | end 133 | 134 | def opts 135 | @opts ||= @tuple.last 136 | end 137 | end 138 | 139 | def be_kind_of(expected) # + args 140 | BeKindOf.new(expected) 141 | end 142 | 143 | def be_section_tuple(key = :any, opts = {}) 144 | BeLineTuple.new(:section, [:key], key, opts) 145 | end 146 | 147 | def be_option_tuple(key = :any, value = :any, opts = {}) 148 | BeLineTuple.new(:option, [:key, :value], key, value, opts) 149 | end 150 | 151 | def be_blank_tuple 152 | BeLineTuple.new(:blank) 153 | end 154 | 155 | def be_comment_tuple(comment = :any, opts = {}) 156 | BeLineTuple.new(:comment, [:comment], comment, opts) 157 | end 158 | 159 | def fixture(fix) 160 | IniParse::Test::Fixtures[fix] 161 | end 162 | end 163 | end 164 | end 165 | 166 | RSpec.configure do |config| 167 | config.include(IniParse::Test::Helpers) 168 | end 169 | -------------------------------------------------------------------------------- /spec/spec_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # -- 4 | # ============================================================================ 5 | # Empty array. 6 | # ============================================================================ 7 | # ++ 8 | 9 | describe 'An empty array' do 10 | it 'should not pass be_section_tuple' do 11 | expect([]).not_to be_section_tuple 12 | end 13 | 14 | it 'should not pass be_option_tuple' do 15 | expect([]).not_to be_option_tuple 16 | end 17 | 18 | it 'should not pass be_blank_tuple' do 19 | expect([]).not_to be_blank_tuple 20 | end 21 | 22 | it 'should not pass be_comment_tuple' do 23 | expect([]).not_to be_comment_tuple 24 | end 25 | end 26 | 27 | # -- 28 | # ============================================================================ 29 | # Section tuple. 30 | # ============================================================================ 31 | # ++ 32 | 33 | describe 'Line tuple [:section, "key", {:opt => "val"}]' do 34 | before(:all) { @tuple = [:section, "key", {:opt => "val"}] } 35 | 36 | it 'should pass be_section_tuple' do 37 | expect(@tuple).to be_section_tuple 38 | end 39 | 40 | it 'should pass be_section_tuple("key")' do 41 | expect(@tuple).to be_section_tuple("key") 42 | end 43 | 44 | it 'should fail be_section_tuple("invalid")' do 45 | expect(@tuple).not_to be_section_tuple("invalid") 46 | end 47 | 48 | it 'should pass be_section_tuple("key", {:opt => "val"})' do 49 | expect(@tuple).to be_section_tuple("key", {:opt => "val"}) 50 | end 51 | 52 | it 'should not pass be_section_tuple("key", {:invalid => "val"})' do 53 | expect(@tuple).not_to be_section_tuple("key", {:invalid => "val"}) 54 | end 55 | 56 | it 'should not pass be_section_tuple("key", {:opt => "invalid"})' do 57 | expect(@tuple).not_to be_section_tuple("key", {:opt => "invalid"}) 58 | end 59 | 60 | it 'should fail be_option_tuple' do 61 | expect(@tuple).not_to be_option_tuple 62 | end 63 | 64 | it 'should fail be_blank_tuple' do 65 | expect(@tuple).not_to be_blank_tuple 66 | end 67 | 68 | it 'should fail be_comment_tuple' do 69 | expect(@tuple).not_to be_comment_tuple 70 | end 71 | end 72 | 73 | # -- 74 | # ============================================================================ 75 | # Option tuple. 76 | # ============================================================================ 77 | # ++ 78 | 79 | describe 'Line tuple [:option, "key", "val", {:opt => "val"}]' do 80 | before(:all) { @tuple = [:option, "key", "val", {:opt => "val"}] } 81 | 82 | it 'should fail be_section_tuple' do 83 | expect(@tuple).not_to be_section_tuple 84 | end 85 | 86 | it 'should pass be_option_tuple' do 87 | expect(@tuple).to be_option_tuple 88 | end 89 | 90 | it 'should pass be_option_tuple("key")' do 91 | expect(@tuple).to be_option_tuple("key") 92 | end 93 | 94 | it 'should fail be_option_tuple("invalid")' do 95 | expect(@tuple).not_to be_option_tuple("invalid") 96 | end 97 | 98 | it 'should pass be_option_tuple("key", "val")' do 99 | expect(@tuple).to be_option_tuple("key", "val") 100 | end 101 | 102 | it 'should pass be_option_tuple(:any, "val")' do 103 | expect(@tuple).to be_option_tuple(:any, "val") 104 | end 105 | 106 | it 'should fail be_option_tuple("key", "invalid")' do 107 | expect(@tuple).not_to be_option_tuple("key", "invalid") 108 | end 109 | 110 | it 'should pass be_option_tuple("key", "val", { :opt => "val" })' do 111 | expect(@tuple).to be_option_tuple("key", "val", { :opt => "val" }) 112 | end 113 | 114 | it 'should fail be_option_tuple("key", "val", { :invalid => "val" })' do 115 | expect(@tuple).not_to be_option_tuple("key", "val", { :invalid => "val" }) 116 | end 117 | 118 | it 'should fail be_option_tuple("key", "val", { :opt => "invalid" })' do 119 | expect(@tuple).not_to be_option_tuple("key", "val", { :opt => "invalid" }) 120 | end 121 | 122 | it 'should fail be_blank_tuple' do 123 | expect(@tuple).not_to be_blank_tuple 124 | end 125 | 126 | it 'should fail be_comment_tuple' do 127 | expect(@tuple).not_to be_comment_tuple 128 | end 129 | end 130 | 131 | # -- 132 | # ============================================================================ 133 | # Blank tuple. 134 | # ============================================================================ 135 | # ++ 136 | 137 | describe 'Line tuple [:blank]' do 138 | before(:all) { @tuple = [:blank] } 139 | 140 | it 'should fail be_section_tuple' do 141 | expect(@tuple).not_to be_section_tuple 142 | end 143 | 144 | it 'should fail be_option_tuple' do 145 | expect(@tuple).not_to be_option_tuple 146 | end 147 | 148 | it 'should pass be_blank_tuple' do 149 | expect(@tuple).to be_blank_tuple 150 | end 151 | 152 | it 'should fail be_comment_tuple' do 153 | expect(@tuple).not_to be_comment_tuple 154 | end 155 | end 156 | 157 | # -- 158 | # ============================================================================ 159 | # Coment tuple. 160 | # ============================================================================ 161 | # ++ 162 | 163 | describe 'Line tuple [:comment, "A comment", {:opt => "val"}]' do 164 | before(:all) { @tuple = [:comment, "A comment", {:opt => "val"}] } 165 | 166 | it 'should fail be_section_tuple' do 167 | expect(@tuple).not_to be_section_tuple 168 | end 169 | 170 | it 'should fail be_option_tuple' do 171 | expect(@tuple).not_to be_option_tuple 172 | end 173 | 174 | it 'should fail be_blank_tuple' do 175 | expect(@tuple).not_to be_blank_tuple 176 | end 177 | 178 | it 'should pass be_comment_tuple' do 179 | expect(@tuple).to be_comment_tuple 180 | end 181 | 182 | it 'should pass be_comment_tuple("A comment")' do 183 | expect(@tuple).to be_comment_tuple("A comment") 184 | end 185 | 186 | it 'should fail be_comment_tuple("Invalid")' do 187 | expect(@tuple).not_to be_comment_tuple("Invalid") 188 | end 189 | 190 | it 'should pass be_comment_tuple("A comment", {:opt => "val"})' do 191 | expect(@tuple).to be_comment_tuple("A comment", {:opt => "val"}) 192 | end 193 | 194 | it 'should fail be_comment_tuple("A comment", {:invalid => "val"})' do 195 | expect(@tuple).not_to be_comment_tuple("A comment", {:invalid => "val"}) 196 | end 197 | 198 | it 'should fail be_comment_tuple("A comment", {:opt => "invalid"})' do 199 | expect(@tuple).not_to be_comment_tuple("A comment", {:opt => "invalid"}) 200 | end 201 | end 202 | --------------------------------------------------------------------------------